Service-ticket application for the Stone-Age.io ecosystem: 816tech (the platform operator / MSP) runs it to support customer organizations. One Go binary embedding PocketBase (system of record, REST API, auth) and a Vue 3 SPA (staff app + requester portal).
The differentiating capability is machine-generated tickets: things and
rule-router publish events inside a customer org's NATS account on
helpdesk.>, the platform's managed-org export delivers them into the
operator hub account as helpdesk.{platformOrgId}.> with unforgeable
subject-based provenance, and the helpdesk's durable JetStream consumer
turns them into tickets. Humans use the portal, staff app, or the
authenticated webhook.
- Two identity classes:
staff(agents/admins, cross-customer) and requesters (users, scoped to one customer). One login page; the router shows the right shell. - Staff workspace: a dashboard landing (queue counts, backlog aging, weekly inflow); a ticket queue with search, status/priority/assignee/customer/category filters, saved views, bulk assign/status, and CSV export; a reports view (time & visits by tech/customer, ticket volume by category and source); and admin for customers, requesters, staff, categories, and notification templates.
- Requester portal: a company dashboard, a searchable list of their own tickets, threaded ticket detail with attachments, a new-ticket form, and a read-only visit schedule.
- Ticketing core: sequential ticket numbers, status/priority/assignee, admin-managed categories + free-text asset/location, comment threads with staff-only internal notes, time entries, site visits.
- Activity & files: workflow changes (status/priority/assignee) recorded to a staff-only audit timeline; file attachments on tickets and comments; a public requester reply on a resolved/closed ticket auto-reopens it.
- Lite dispatch: promote a ticket to on-site work with a
requestedvisit (no tech/time yet), schedule it from the staff Dispatch view (needs-scheduling bucket + day-grouped list), free-text visit location. Requesters see their visits read-only in the portal. - Customers directory: platform-org mapping for NATS ingestion, per-customer webhook tokens (admin reveal/rotate).
- Outbound email: DB-stored templates (Go
text/template, editable in the SPA) fired from record hooks — created / assigned / commented / status changed / visit scheduled / rescheduled / canceled — with per-event recipient specs, a send log, and day-keyed dedupe. No SMTP configured = clean no-op. See docs/notifications.md. - Inbound machine tickets: NATS durable consumer + authenticated
webhook (
POST /api/helpdesk/inbound/{token}), both idempotent viadedupe_key. See docs/protocol.md. - Throughout the SPA: live updates (PocketBase realtime subscriptions), light/dark themes, keyboard shortcuts, responsive table-to-card layouts, and self-service profile edits + forgot-password reset.
The SPA is //go:embed-ed at compile time; the committed
internal/webui/public means a fresh checkout builds without npm — but
rebuild and re-commit it whenever ui/ changes.
cd ui && npm ci # once
npm run build # vue-tsc + vite → ../internal/webui/public (commit the output)
cd .. && go build ./cmd/helpdesk
./helpdesk serve # UI at http://127.0.0.1:8090/ · PocketBase admin at /_First start seeds a bootstrap staff admin (admin@helpdesk.local) and
prints its password once. Configuration is helpdesk.yaml +
HELPDESK_* env overrides — see
docs/configuration.md. SMTP (outbound email) and
the application URL (ticket links in emails) are configured in the
PocketBase dashboard, not the YAML.
The UI is rebrandable at runtime without a rebuild: point branding.dir
(env HELPDESK_BRANDING_DIR) at a host directory of theme.css / logo.svg /
branding.json to override the app name, logo, and theme — see
docs/configuration.md and the
branding.example/ template.
go test ./...cmd/helpdesk/ PB bootstrap, OnServe wiring, embedded UI, retention cron
config/ viper Config (HELPDESK_ env prefix)
migrations/ Go schema-as-code (collections, rules, seeds)
internal/
authz/ access-rule vocabulary shared by migrations + routes
tickets/ ticket-number assignment + field defaults, auto-reopen
visits/ visit status defaulting + scheduled-visit invariant
activity/ ticket_events audit trail (status/priority/assignee)
authfix/ auth-default fixups (email visibility on create)
notifications/ notifier core, templates, lifecycle hooks, editor API
subjects/ NATS subject grammar (helpdesk.{org}.tickets.{verb})
natsx/ NATS connect (creds file) + inbox stream helper
ingest/ durable consumer → ticket projection
inbound/ webhook route + webhook-token reveal/rotate
webui/ //go:embed all:public (committed SPA dist)
testutil/ real-PB-against-t.TempDir() test harness
ui/ Vue 3 + Vite + Pinia + Tailwind + daisyUI SPA
docs/ plan, data model, wire protocol, notifications, config
- Standalone sibling app (kiosk / access-control pattern), deliberately not a platform feature: helpdesk agents never hold control-plane credentials, and the tenancy axes differ (platform tenant = customer org; helpdesk tenant = the MSP).
- Tenancy is plain collection rules —
customers+users.customer+ staff roles (internal/authz). No pb-tenancy. See docs/data-model.md. - NATS is best-effort: the app boots and serves portal/webhook traffic without a broker; the durable consumer resumes where it left off.
- The org id in a machine ticket comes from the subject (rewritten by the operator-signed platform import), never the payload.