Add Meteor 3 + wormhole backend PoC with live DDP tickets#357
Open
horner wants to merge 27 commits into
Open
Conversation
- 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.
8 tasks
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.
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
rs0) in docker-compose (required for oplog tailing)vendor/meteor-wormhole: git submodule, wired viaMETEOR_PACKAGE_DIRSmeteor-backend/(headless, server-only):auth.bridgemethod binds a DDP connection to the signed-in user using the sharedsessioncollection)tickets.list/create/updateStatusmethods +tickets.byTeampublicationclock.active/start/stopmethods (break-deduction logic ported verbatim) +clock.liveForTeamspublication/api(Swagger at/api/docs), MCP at/mcp— all 6 methods exposed with JSON Schemassrc/lib/ddp.ts— dependency-free DDP client with EJSON decoding ($date, ObjectId) +useLiveTickets/useLiveClockEventshooksTicketsPage— replaced the/v1/tickets/wsWebSocket effect with a DDP subscription (render logic untouched)seed.tsnow emits migrated shapes (assignedToas array, teams withorgId)Validation
tickets_create/tickets_updateStatusvia wormhole REST → board updates instantly, no reloadinitialize+tools/listreturns all 6 tools; full clock start/stop cycle verified via RESTlint,typecheck,format, and all 53 frontend tests pass; backend typecheck clean/v1/*routes unchanged and running concurrentlyOut of scope (Phase 2)
Auth/OIDC migration, remaining feature cutover, retiring Fastify — tracked in the Phase 2 plan.