In 0.8.0 we built a platform. In 0.8.1 we made it fit in your pocket.
GoatFlow now works on an iPhone SE. That might not sound like a headline feature, but if you’ve ever tried to triage a P1 ticket from a 375px screen while standing in a queue at the supermarket, you know it matters. Tables collapse gracefully, buttons are big enough to actually hit with a thumb, the rich text editor doesn’t fight you, and when the train goes into a tunnel you get an offline page instead of a white screen.

Mobile Optimization
Every page got a mobile pass. Not a “we added a viewport meta tag” pass — a “we tested on a 375px screen and fixed everything that broke” pass.
Tables were the biggest problem. The agent ticket list has eleven columns. On a phone, that’s a horizontal scroll disaster. Now secondary columns hide below breakpoints:
| Viewport | Visible columns |
|---|---|
| Desktop (1024px+) | All 11 columns |
| Tablet (768px) | Checkbox, ticket#, subject, state, priority, actions |
| Phone (<768px) | Same — queue, customer, assigned, article count, age hidden |
The same treatment applies to the customer ticket list (hides queue, customer, agent, updated) and admin user management (hides groups, 2FA status, last login).
Touch targets were the second biggest problem. Admin action buttons were 36px — below the 44px WCAG 2.5.8 minimum. The new .gk-action-btn class enforces min-width: 44px; min-height: 44px and consolidates the repeated inline classes across all admin pages.

Cards, modals, and forms all got tighter mobile padding via a @media (max-width: 767px) block in the component CSS. This is a single-file change that propagates to every page using .gk-card-body, .gk-modal-header, .gk-modal-footer, and .gk-table — no need to touch 49 individual admin templates.
The dashboard now uses GridStack’s responsive breakpoints: 1 column on phones, 6 on tablets, 12 on desktop. The widget lock toggle hides on mobile because drag-and-drop with a thumb is nobody’s idea of fun.
Ticket creation forms got collapsible tips (Alpine.js toggle on mobile, always visible on desktop), stacked action buttons, compact file upload zones, and larger Tiptap toolbar buttons.

Page headings scale from 1.5rem on phones to 1.875rem on desktop. Profile pages, admin pages, and ticket detail all benefit.
PWA & Push Notifications
GoatFlow is now installable as a Progressive Web App. The manifest, service worker, and icons ship with every deployment.
Browser GoatFlow Push Service
│ │ │
│ GET /manifest.json │ │
│◄─────────────────────────│ │
│ │ │
│ Register /sw.js │ │
│─────────────────────────►│ │
│ │ │
│ GET /api/push/vapid-key │ │
│─────────────────────────►│ │
│ { publicKey: "B..." } │ │
│◄─────────────────────────│ │
│ │ │
│ PushManager.subscribe() │ │
│──────────────────────────┼───────────────────────────►│
│ { endpoint, keys } │ │
│◄─────────────────────────┼────────────────────────────│
│ │ │
│ POST /api/push/subscribe│ │
│─────────────────────────►│ │
│ │ (stored in DB) │
│ │ │
│ ... later ... │ │
│ │ Pending reminder fires │
│ │ webpush.Send() ──────────►│
│ │ │
│ push event ◄────────────┼────────────────────────────│
│ showNotification() │ │
The service worker caches static assets (CSS, JS, fonts, icons) with a cache-first strategy and serves an offline fallback page when navigation requests fail. Push events parse a JSON payload and display a browser notification with the ticket title, queue name, and a direct link.
VAPID keys are generated automatically on first boot. For production, set GOATFLOW_PUSH_VAPID_PUBLIC_KEY and GOATFLOW_PUSH_VAPID_PRIVATE_KEY as environment variables so subscriptions survive restarts.
The notification bell appears in the navbar for authenticated users. One click subscribes; another unsubscribes. The bell glows primary when active, stays muted when off.
Push dispatch hooks into the existing pending reminder scheduler. When a reminder fires, the in-memory hub dispatches to the polling frontend as before, and the new DispatchPushReminder function sends webpush notifications to all subscribed browsers. Stale subscriptions (HTTP 404/410 from expired endpoints) are automatically cleaned up.
Three API endpoints power the subscription lifecycle:
| Endpoint | Method | Purpose |
|---|---|---|
/api/push/vapid-key | GET | Returns the VAPID public key |
/api/push/subscribe | POST | Stores a push subscription |
/api/push/unsubscribe | DELETE | Removes a subscription |
Security Hardening
Three tightening measures that should have been there from the start:
CORS — replaced the wildcard Access-Control-Allow-Origin: * with origin validation against CORS_ALLOWED_ORIGINS. Defaults to same-origin when unset. No more “any website can call our API.”
JWT — production mode now rejects weak or placeholder secrets. If APP_ENV=production and the JWT secret is under 32 characters or contains keywords like secret, changeme, or test, GoatFlow refuses to start. Better to fail at boot than to discover your tokens are forgeable.
Dependencies — make check-deps runs bun pm audit / npm audit as part of the test pipeline. Known vulnerabilities in frontend dependencies now block CI.
SQL Dialect Portability
Plugin authors no longer need to think about which database they’re running on. The query pipeline automatically rewrites MySQL-specific functions for PostgreSQL and vice versa:
| MySQL | PostgreSQL |
|---|---|
DATE_SUB(expr, INTERVAL n UNIT) | (expr - INTERVAL 'n unit') |
DATE_ADD(expr, INTERVAL n UNIT) | (expr + INTERVAL 'n unit') |
UNIX_TIMESTAMP() | EXTRACT(EPOCH FROM NOW())::bigint |
CURDATE() | CURRENT_DATE |
This runs transparently on every query — plugin SQL and core SQL both benefit.
Plugin Infrastructure
Three new capabilities for plugin developers:
Sidecar containers — gRPC plugins can declare companion containers in their plugin.yaml. In Kubernetes, sidecars are injected into the plugin pod spec with shared localhost networking. In Docker Compose, GenerateComposeFragment() produces the service definitions. The first consumer is goatkit-devices, which declares an ADB server sidecar.
Custom field atomic operations — CustomFieldsSet() now supports five operations beyond simple value assignment: increment (with floor/ceiling), append and remove for multi-select fields, cas (compare-and-swap) for optimistic concurrency, and toggle for booleans. Backward compatible — plain values still work.
SSE channels — HostAPI.PublishEvent(channel, eventType, data) pushes events to named, per-plugin channels. Clients subscribe at /api/v1/plugins/{name}/events/{channel}. 30-second keepalive comments prevent proxy timeouts.
Coachmarks
Seven onboarding tips now greet new users as they explore the interface:
| Tip | Where it appears | What it explains |
|---|---|---|
| Theme switcher | Everywhere | Theme and colour mode selection |
| Dashboard widgets | Dashboard | Widget customisation |
| Create a ticket | Dashboard, Tickets | Ticket creation workflow |
| Filter and search | Tickets | Search bar and filters |
| Bulk actions | Tickets | Multi-select operations |
| Queue overview | Dashboard | Queue management |
| Push notifications | Everywhere | Browser notification opt-in |
Each tip appears a maximum of 2–3 times, uses staggered delays so they don’t compete, and respects the “Reset feature highlights” toggle on the profile page. All seven tips are translated in all 15 languages.
By the Numbers
- 66 admin pages with mobile-optimized headings (CSS-level, zero template changes)
- 49 admin tables with tighter mobile padding (same)
- 7 onboarding coachmarks across 15 languages (210 translated strings)
- 3 push notification API endpoints
- 1 service worker, 1 offline fallback page, 1 web app manifest
- 1 new Go dependency (
webpush-go) - 1 database migration (
gk_push_subscription) - 0 breaking changes
What’s Next
0.8.2 (June 2026) picks up the items that didn’t make this cut: per-plugin service worker caching, admin UI for plugin UIs, WebAuthn/FIDO2 hardware key support, and performance benchmarking.
0.9.0 (August 2026) ships the first-party open source plugins: FAQ/Knowledge Base, Calendar & Appointments, and Process Management with visual workflow designer.
1.0.0 (November 2026) is the production release.
Bonus Track: Testing Flaky Fixtures
The most instructive bug in this release wasn’t a feature bug — it was a test infrastructure bug. The MCP authorization tests use shared database fixtures (groups, users, queue permissions) that other test packages can wipe between runs. The fixture validation checked “does at least one admin group membership exist?” — but the admin user needs three memberships (admin, support, billing). If two out of three got wiped, the validation passed, the fixtures weren’t rebuilt, and the admin user couldn’t see all queues.
The fix was two lines: check membershipCount < 3 instead of membershipCount == 0, and ensure the admin group (ID 2) exists before inserting foreign key references. Simple, obvious in hindsight, and the kind of thing that only surfaces when you run the full suite 50 times in a row.
Shared mutable state in tests is the same problem as shared mutable state everywhere else. The only winning move is to make your validity checks as paranoid as your production code.
- Source & containers: GoatFlow on GitHub
- Full changelog: CHANGELOG.md
- Helm chart:
oci://ghcr.io/goatkit/charts/goatflow
Questions? Feedback? Open a GitHub Discussion and let us know what you think!