Hermes
Grails Guru & Self-Service Portal Advisor. Expert guidance on Grails/Groovy views (GSP), and self-service portal architecture.
A Quick Note On Working with Hermes
Hermes is your Grails Guru & Self-Service Portal Advisor — expert guidance on Grails/Groovy views (GSP), and self-service portal architecture. In practice, the work he handles is centered on data-driven admin interfaces and web portals. Think CRUD-heavy admin panels with tables, filters, status badges, and document/payment flows — Grails GSP views + layouts, GORM/Hibernate for domain modeling, daisyUI 5 + Tailwind 4 for the UI layer, and a built-in theme switcher.
The chat session below is a real-world example of how a conversation with Hermes might look — someone who saw the one-click Billing Portal in the Seed-Data video and wants the same thing with their own invoice data model and their own branding. Read it, pick up the rhythm.
But you don't have to follow any of it. You don't need a billing portal. Just open the chat with Hermes and tell him what you need — an admin dashboard, a self-service portal, a document viewer, whatever Grails web app you have in mind. He'll adapt.
Go chat with Hermes. Go build stuff.
Billing Portal — Make It Yours
How the crew works — in theory vs. in practice. In theory, you start with Athena: she gathers requirements, writes a PRD, and hands the spec to a specialist (see Athena → New Billing Portal). In practice, for a well-trodden ask like an invoice portal, people walk straight up to the specialist — "I saw the Seed-Data video, I want that, but mine." Both paths are fine. This page shows the direct one. If the model gets complex, Hermes loops Athena in.
The key idea: you don't rebuild — you re-skin. The Billing Portal in the video is a shipped
example (_apps/flowkraft/99-custom/_examples/billing-portal-grails/_custom/). Making it yours is
copying its _custom/ folder and changing two things: your data model and your brand. The
app-seed.groovy does the rest — it scaffolds the app from the grails-playground blueprint, applies
your overrides/, and writes the DataPallas push report that feeds invoices in over REST. No
"build on a copy, then hand-delete the sample code" — that step is gone.
The Ask — "make it mine"
Absolutely — and here's the good news: you don't rebuild it. That portal in the video is a shipped example. You copy its _custom/ folder, change exactly two things — your data model and your brand — and run it from Seed Data, just like the video. Two-thirds of the work already exists.
One note on process: in theory, for a brand-new or complex model I'd send you to Athena first — she'd write a quick PRD and hand the spec back to me. In practice, an invoice portal is well-trodden ground, so let's go direct. If we hit anything gnarly, we loop Athena in.
The Shipped Example & the _custom Convention
Start from the shipped example. It lives at _apps/flowkraft/99-custom/_examples/billing-portal-grails/_custom/. A custom app is just four things — copy them to your own folder and edit in place:
| 1 | _apps/my-billing-portal/_custom/ |
| 2 | ├── app.json # id, name, port, launch links, tablePrefix, apiKey |
| 3 | ├── app-seed.groovy # scaffolds the app + writes the DataPallas push report |
| 4 | ├── overrides/ # ONLY your changed files (domain, controllers, views, brand) |
| 5 | └── README.md # the recipe |
Copy the example's _custom/ to _apps/my-billing-portal/_custom/. Everything unchanged — the Grails build, Dockerfile, layouts, theme picker, payments — comes from the grails-playground blueprint at scaffold time. You never fork the whole app; overrides/ holds only the files you actually touch.
Your Data Model
That maps almost 1:1 onto the example's bp_* model. There are three touch-points, all in overrides/, all following the existing pattern:
- The domain —
overrides/grails-app/domain/com/flowkraft/Invoice.groovy(plusCustomer/InvoiceLine). Add your fields. - The REST ingest —
ApiInvoiceControlleratPOST /api/invoices/ingest. It upserts the customer by email and the invoice by number; add your new fields to the payload it accepts. - The push query — in
app-seed.groovy'swritePushReport, the SQL that reads your source rows and maps columns → the ingest payload.
The domain — add vatId and you're done:
| 1 | class Invoice { |
| 2 | String invoiceNumber |
| 3 | Date invoiceDate |
| 4 | Date dueDate |
| 5 | String status // PAID / DUE / OVERDUE |
| 6 | String vatId // ← your new field |
| 7 | BigDecimal subtotal, tax, total |
| 8 | |
| 9 | static belongsTo = [customer: Customer] |
| 10 | static hasMany = [lines: InvoiceLine] |
| 11 | } |
DataPallas bursts your source invoices and curls each one to /api/invoices/ingest — the portal renders them. So the model, the ingest, and the push SQL just have to agree on field names. Add vat_id to the ingest payload and to the writePushReport query, and every invoice carries your VAT id end to end.
Your Branding
Three small edits, all in overrides/. It's daisyUI 5 + Tailwind 4, so a whole re-theme is one word:
- Theme —
views/common/_themeInit.gsp: setRB_DEFAULT_THEME = 'corporate'. daisyUI shipscorporate,business,nord… —corporateis the clean light one. - Logo / wordmark —
views/layouts/portal.gsp+admin.gsp: swap the DataPallas logo for your mark. - Invoice biller —
views/_invoiceDocument.gsp: your company name + address.
| 1 | <!-- views/common/_themeInit.gsp --> |
| 2 | window.RB_DEFAULT_THEME = 'corporate'; |
| 3 | |
| 4 | <!-- views/layouts/portal.gsp — navbar brand --> |
| 5 | <a href="${createLink(uri: '/portal')}" class="flex items-center gap-2 no-underline"> |
| 6 | <span class="inline-flex items-center justify-center w-8 h-8 rounded-lg |
| 7 | bg-primary text-primary-content font-bold text-lg">A</span> |
| 8 | <span class="text-xl font-bold tracking-tight">Acme Freight</span> |
| 9 | </a> |
The badge uses theme colours (bg-primary text-primary-content), so it recolours with the theme automatically. That's the whole re-skin — one theme word, one badge letter, one company name. The DOM ids don't change, so it stays automation-friendly.
The Bundle
Here's your _custom/ — drop it at _apps/my-billing-portal/_custom/. The identity lives in app.json:
| 1 | { |
| 2 | "id": "my-billing-portal", |
| 3 | "name": "Acme Freight — Billing Portal", |
| 4 | "type": "docker", |
| 5 | "url": "http://localhost:8600", |
| 6 | "tablePrefix": "bp_", |
| 7 | "apiKey": "acme-portal-key-CHANGE-ME", |
| 8 | "blueprint": "flowkraft/grails-playground", |
| 9 | "visible": false, |
| 10 | "tags": ["grails", "portal", "billing", "custom-app"] |
| 11 | } |
app-seed.groovy and overrides/ you carried from the example with your two edits (the vatId field and the Acme brand); the README.md is the recipe. Nothing else to write — the seed script does the scaffolding and writes the push report on its own.
Run It — one click from Seed Data
Exactly. Open DataPallas → any database connection → Seed Data tab → run Acme Freight — Billing Portal. The script scaffolds the app, writes the push report, and reveals the app card. Then:
- Apps Manager → your card appears → Start (Docker) → Launch →
/portaland/admin. - Processing → Generate Reports → pick Billing Portal Push (Acme) → Burst.
- Your source invoices flow over REST into the portal — customers log in and pay, admins do CRUD. Auto-created logins, correct statuses, no duplicates on re-run.
Same one-click feel as the video — but your data model, your brand.
Done
Anytime. Notice what you didn't do: you never left daisyUI/Tailwind, never touched the plumbing, never hand-deleted sample code. Two edits and a Seed-Data run.
If you later want the identical portal in React instead of Grails, my colleague Apollo ships the same thing on Next.js — same routes, same DOM ids, same seeded data. Go build.