Skip to content

Add Meteor 3 + wormhole backend PoC with live DDP tickets#357

Open
horner wants to merge 27 commits into
mainfrom
meteor-is-back
Open

Add Meteor 3 + wormhole backend PoC with live DDP tickets#357
horner wants to merge 27 commits into
mainfrom
meteor-is-back

Conversation

@horner

@horner horner commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Phase 1 proof of concept: a headless Meteor 3 backend (meteor-backend/, port 3100) running alongside the existing Fastify backend on the same MongoDB, using wreiske:meteor-wormhole (vendored as a git submodule) to auto-expose Meteor methods as REST + OpenAPI + MCP, and DDP pub/sub for live data.

The key result: a ticket created or mutated by any writer — wormhole REST, an MCP agent, the Fastify backend, or mongosh — appears on every open Tickets board within ~1s via oplog tailing, with zero broadcast code on any server. This is the pattern intended to replace the 8 hand-built Fastify WebSocket endpoints.

What's included

  • Infra: MongoDB now runs as a single-node replica set (rs0) in docker-compose (required for oplog tailing)
  • vendor/meteor-wormhole: git submodule, wired via METEOR_PACKAGE_DIRS
  • meteor-backend/ (headless, server-only):
    • better-auth session bridge (auth.bridge method binds a DDP connection to the signed-in user using the shared session collection)
    • Tickets: tickets.list/create/updateStatus methods + tickets.byTeam publication
    • Clock: clock.active/start/stop methods (break-deduction logic ported verbatim) + clock.liveForTeams publication
    • Wormhole: REST at /api (Swagger at /api/docs), MCP at /mcp — all 6 methods exposed with JSON Schemas
  • Frontend:
    • src/lib/ddp.ts — dependency-free DDP client with EJSON decoding ($date, ObjectId) + useLiveTickets / useLiveClockEvents hooks
    • TicketsPage — replaced the /v1/tickets/ws WebSocket effect with a DDP subscription (render logic untouched)
  • Seed fix: seed.ts now emits migrated shapes (assignedTo as array, teams with orgId)

Validation

  • Live end-to-end in browser: tickets_create / tickets_updateStatus via wormhole REST → board updates instantly, no reload
  • MCP initialize + tools/list returns all 6 tools; full clock start/stop cycle verified via REST
  • lint, typecheck, format, and all 53 frontend tests pass; backend typecheck clean
  • Existing Fastify /v1/* routes unchanged and running concurrently

Out of scope (Phase 2)

Auth/OIDC migration, remaining feature cutover, retiring Fastify — tracked in the Phase 2 plan.

horner added 6 commits June 12, 2026 19:40
- Vendor wreiske:meteor-wormhole as git submodule at vendor/meteor-wormhole
- Run MongoDB as single-node replica set (rs0) in docker-compose for oplog tailing
- Add headless Meteor 3 app at meteor-backend/ (port 3100) sharing the existing Mongo:
  - better-auth session bridge (auth.bridge method + per-connection identity)
  - Tickets methods (list/create/updateStatus) + tickets.byTeam publication
  - Clock methods (active/start/stop) + clock.liveForTeams publication
  - Wormhole REST (/api + Swagger docs) and MCP (/mcp) exposure of all 6 methods
- Add dependency-free DDP client (src/lib/ddp.ts) with EJSON decoding and live hooks
- TicketsPage: replace hand-rolled /v1/tickets/ws WebSocket with DDP subscription
- seed.ts: emit migrated shapes (assignedTo as array, teams with orgId)
- Ignore vendor/ and meteor-backend/ in ESLint and Prettier
- meteor-backend: add tickets.update/delete/assign/batchStatus, reviewed
  semantics, github param, creator auto-assign, and portable activity-log
  emission (shared 'activities' collection) matching the Fastify service
- expose new methods via wormhole REST/MCP; add CORS for the Vite origin
- frontend: ticket mutations (create/update/status/delete/batch) now go
  through wormhole REST; assignTicket stays on Fastify for notification
  fan-out
- clock UI cutover: TeamContext + WorkPage drop the /v1/clock/ws WebSocket
  for the oplog-backed clock.liveForTeams DDP publication
- DdpClient: auto-reconnect with backoff, re-auth, and sub restore
- vendor/meteor-wormhole → feat/invocation-context (mieweb/meteor-wormhole#4):
  AsyncLocalStorage per-invocation context exposing the caller's
  Authorization header to methods invoked over REST/MCP
- auth-bridge: requireIdentity resolves explicit sessionToken param (legacy),
  then currentBearerToken() from the header, then the DDP connection cache
- main.js: drop sessionTokenProp from all wormhole schemas — tokens no
  longer advertised in Swagger/MCP tool schemas
- wormholeCall: send Authorization: Bearer header instead of a sessionToken
  body field (methods still accept the body param for one release)
- add meteor-to-production-plan.md — trackable M0–M4 production plan

Validated: REST + MCP tool calls with header-only auth, UI ticket create,
legacy body-token path, unauthenticated rejection; lint/typecheck/format/53
tests green
- backend auth: enable better-auth jwt plugin (15m access tokens) and keep
  JWKS endpoint at /api/auth/jwks
- Fastify require-auth: accept JWT bearer tokens via jose + remote JWKS local
  verification (no DB token lookup path for JWTs)
- Meteor auth bridge: replace session-collection reads with JWT verification
  against JWKS, keep PAT validation path (th_pat_) via personal_access_tokens,
  and retain DDP connection identity cache
- frontend api: add getAccessToken() cache and use JWT bearer auth for
  wormholeCall
- frontend DDP: auth.bridge now uses JWTs and proactively re-bridges before exp
- update meteor-to-production-plan.md: mark M0.c checklist complete

Validated:
- JWT claims include sub/email/name/iss/aud/exp with 15m TTL
- Meteor REST + MCP tool calls work with JWT bearer
- Fastify /v1 routes accept JWT bearer
- Meteor PAT path works; legacy session bearer now rejected by Meteor
- Browser smoke test: sign in + create ticket via UI with JWT-backed flow
- lint/typecheck/format/tests green
Methods now resolve identity only from the Authorization: Bearer header
(REST/MCP) or the DDP connection cache (auth.bridge). Dropped the
sessionToken body param from all tickets.* and clock.* methods and the
requireIdentity(methodContext) signature; refreshed auth-bridge docs.
Completes the final M0.b overlap-cleanup checkbox.
Backend (Fastify/better-auth IdP only):
- auth.ts builds socialProviders from env (github/google/apple), each
  self-registering only when its *_CLIENT_ID is present.
- Authentik (any OIDC IdP) via the genericOAuth plugin, registered as
  providerId 'authentik' when AUTHENTIK_CLIENT_ID is set.
- server.ts: allow 'apple' in the sign-in/social + callback provider enums.

Frontend:
- socialProviders.ts registry gated by VITE_SOCIAL_PROVIDERS.
- LoginForm renders the configured provider buttons via @mieweb/ui Button.
- api.ts: signInWithSocial accepts apple; new signInWithOAuth2 for genericOAuth.

Docs/config: backend/README env vars; docker-compose VITE_SOCIAL_PROVIDERS.
horner and others added 10 commits June 12, 2026 22:17
Add meteor-backend/server/permissions.js — a faithful port of
backend/src/lib/permissions.ts. It builds the same CASL ability
(buildAbilityFor) and exposes buildTeamAbility, requireTeamMembership,
and requireTicketPermission, resolving org-role and enterprise-elevation
via the native driver.

Rewire tickets.js and clock.js to the shared guards and remove the
duplicated local requireTeamMembership/requireAuthorizedTicket helpers.
Per-ticket methods now authorize by CASL action (update/delete/assign),
bringing Meteor methods to authorization parity with Fastify — notably
only a ticket's creator (or an org/enterprise-elevated user) may delete.
Add meteor-backend/server/email.js — a nodemailer wrapper that mirrors
backend/src/lib/email.ts, reading the same SMTP_*/EMAIL_FROM env. Imported
from main.js so it is bundled and validated; a Meteor consumer follows in
M1 (clock/notification mail). Adds nodemailer to meteor-backend deps.
Add meteor-backend/server/push.js — port of backend/src/services/push.service.ts.
sendToUser fans a notification out to iOS (APNs), Android (FCM) and web
(VAPID) targets, reading the same pushsubscriptions/devicetokens collections
and the same VAPID_*/APNS_*/FIREBASE_SERVICE_ACCOUNT env. Imported from main.js
for bundling/validation; a Meteor consumer follows with notifications in M1.

firebase-admin's exports map is not honored by Meteor's bundler, so the app
API is taken from the package root and getMessaging from its concrete
lib/messaging path. Adds web-push, @parse/node-apn, firebase-admin deps.
…stence)

Extract shared break math into clock-core.js (DRY between clock.* methods
and the auto-clockout jobs), and add agenda.js porting the four clock jobs
from backend/src/services/agenda.service.ts: shift-4h-reminder,
shift-end-reminder, shift-auto-clockout, shift-missed-clockout.

The Agenda backend reuses Meteor's already-open MongoDB connection
(MongoBackend({ mongo: rawDb() })) against the shared agendajobs collection,
so it never opens a second client. The processor loop only starts when
METEOR_AGENDA_ENABLED=true, leaving Fastify the sole processor during
coexistence; M1 flips it on when Meteor becomes the clock writer.
Add meteor-backend/Dockerfile.dev (node:24-bookworm-slim + pinned Meteor
3.4.1; Alpine/musl can't run Meteor) and a docker-compose meteor-backend
service that bind-mounts the repo, installs deps on start, and runs
meteor run on port 3100. Wires container-internal MONGO_URL/MONGO_OPLOG_URL
to the rs0 mongodb service, METEOR_PACKAGE_DIRS for the vendored wormhole
package, and CORS_ORIGINS for the /api bridge. Adds VITE_METEOR_URL to the
frontend service so the SPA can reach the Meteor backend.

Validated: image builds and the container boots cleanly (agenda processor
gated off, /api/docs 200).
Add timers.liveForUser publication (user's running timers, endTime:null) and
register a Timers collection. WorkPage now subscribes via DDP and refetches the
day/week on any timers collection change, mirroring the clock.liveForTeams
cutover. Timer mutations and day/week reads stay on Fastify REST during
coexistence. Removes the now-unused timerApi.openLiveStream WS client.
Add notifications.liveForUser publication (user's inbox, newest 200) and a
Notifications collection. New shared DDP helper subscribeNewNotifications fires
for notifications delivered live (after the initial backlog snapshot). main.tsx,
NotificationsPage, and ShiftReminderContext now consume it instead of
notificationApi.openStream, which is removed. Fastify remains the notification
writer and owns push fan-out during coexistence; mutations stay on REST.
Port the entire Fastify ClockService onto Meteor as the clock-domain writer.

Backend (meteor-backend):
- clock.js: all clock.* methods (start/stop/pause/resume/status/active/
  activeForUser/events/timesheet/updateTimes/deleteEvent/createManual/
  agreeAutoClockout/respondShiftReminder) with the full side-effect pipeline.
- clock-core.js: rich toPublicClockEvent + break helpers (toBreakEntries,
  normalizeBreakEntries, computeTotalBreakSeconds, findBreaksForEvents).
- timer-core.js (new): coupled timer close-out/restart on pause/resume/stop.
- notify-core.js (new): createNotification + notifyClockAdmins (agenda.js now
  reuses createNotification, DRY).
- activity-core.js (new): emitActivity clock.in/clock.out logging.
- main.js: wormhole-expose all new clock methods.

Coexistence flip (C3): Meteor Agenda processor ON by default
(METEOR_AGENDA_ENABLED), Fastify processor gated OFF
(FASTIFY_AGENDA_ENABLED!=true) so the shared agendajobs collection is never
double-processed. docker-compose wires both flags.

Frontend:
- clockApi cut over from Fastify /v1/clock/* REST to wormhole method calls;
  dropped the dead clock WS openLiveStream (clock live state already via DDP).
- agreeClockout -> clock.agreeAutoClockout; ShiftReminderContext tolerant of
  the wormhole not-found/already-closed reason shape.

Validated: lint, typecheck, prettier, 53 frontend tests; live REST exercise of
the full clock lifecycle (start/pause/resume/stop/manual/timesheet/update/
delete) with confirmed accumulatedTime math, agenda job schedule+cancel,
activity log, and admin/self notifications.
- Add assignee notification fan-out + resolved assigneeName to the Meteor
  tickets.assign method (createNotification fires a single push per newly
  added assignee, fixing the Fastify double-push).
- Cut frontend ticketApi.assignTicket over to wormhole REST.
- Audit M1 Fastify route deletion: all four route files still serve live
  consumers (team dashboard, WorkPage reads, notification inbox/invites,
  ticket reads), so deletion is deferred to M2/M3. Documented in plan.
mfisher31 and others added 10 commits June 18, 2026 09:43
move: activity and presense features to Meteor
Port 18 Fastify REST endpoints + 3 WebSocket streams to Meteor methods
and DDP publications. Teams (12 methods + teams.byUser pub), Messages
(2 methods + messages.byThread pub), Channels (4 methods +
channelmessages.byChannel pub + ensureDefaultChannel helper).

Org auto-provisioning ported to org-helpers.js for team create/join.
Frontend teamApi, messageApi, channelApi cut over to wormholeCall.
TeamContext.tsx and MessagesPage.tsx cut over from WebSocket to DDP
subscriptions for real-time updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port ~35 Fastify REST endpoints to Meteor methods:
- Users (6): profile get/update, username check/claim, batch lookup
- Organizations (20): CRUD, member management, CASL ability checks,
  admin/public default-org endpoints, slug validation, search
- Enterprises (7): CRUD, member role management, user search
- PATs (3): list, create (sha256 hash), revoke with activity log

Frontend userApi, usernameApi, orgApi, orgAdminApi, enterpriseApi,
tokenApi all cut over to wormholeCall. Avatar/background uploads and
GET /v1/me stay on Fastify (deferred to M4).

Add Vitest integration test suite for the Meteor wormhole REST API:
- tests/tickets.test.ts (10 tests), teams.test.ts (11), clock.test.ts (9)
- Test infrastructure: helpers.ts (auth + wormhole wrapper), setup.ts
- scripts/checks.sh gains `meteor` job for CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port file uploads and media management from Fastify to Meteor:

- Static file serving via WebApp.connectHandlers at /uploads/*
- Avatar/background upload+delete via multipart handlers (busboy)
- Media library: image upload, thumbnail upload, CRUD methods
- Attachments: metadata-only wormhole methods (list, add, remove)

Frontend api.ts cut over: attachmentApi, mediaApi (upload + CRUD),
userApi avatar/background all point to Meteor. toAbsoluteUrl routes
/uploads/ paths through METEOR_BASE_URL.

Fix video thumbnail regeneration: fetch video with auth credentials
into a blob URL to avoid cross-origin canvas tainting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mfisher31 mfisher31 moved this from Ready to In Progress in Scrum Team Jerry Jun 22, 2026
@Dharp02 Dharp02 mentioned this pull request Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants