Skip to content

GoatFlow 0.8.1: The Pocket Release

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.

iPhone SE - Ticket View 1

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:

ViewportVisible 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.

iPhone SE - Ticket View 2

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.

iPhone SE - Ticket View 2

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:

EndpointMethodPurpose
/api/push/vapid-keyGETReturns the VAPID public key
/api/push/subscribePOSTStores a push subscription
/api/push/unsubscribeDELETERemoves 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.

Dependenciesmake 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:

MySQLPostgreSQL
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 operationsCustomFieldsSet() 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 channelsHostAPI.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:

TipWhere it appearsWhat it explains
Theme switcherEverywhereTheme and colour mode selection
Dashboard widgetsDashboardWidget customisation
Create a ticketDashboard, TicketsTicket creation workflow
Filter and searchTicketsSearch bar and filters
Bulk actionsTicketsMulti-select operations
Queue overviewDashboardQueue management
Push notificationsEverywhereBrowser 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.


Questions? Feedback? Open a GitHub Discussion and let us know what you think!

Back to Blog