Add Meteor 3 + wormhole backend PoC with live DDP tickets#357
Merged
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>
- Update ddp.ts to subscribe to 'users' instead of 'user' - Fix TeamContext user queries - Update EnterprisePage and InstallerModal user references
- Migrate test helpers to use 'users' collection - Update all Meteor backend tests to use correct user references - Add bcrypt dependency for test password handling
- Simplify signup spec by removing redundant wait operations - Update blocking tests to use cleaner assertions - Improve test page objects (LoginPage, SignupPage) - Remove unnecessary delays and improve test reliability
- Remove unnecessary wait operations from all realtime tests - Update DDP subscriptions to use 'users' collection - Improve assertion patterns for reactive data - Simplify timer and timesheet test logic
- Refactor team invitation tests with cleaner assertions - Simplify work summary tests - Remove redundant waits and improve test flow
- Add global setup/teardown hooks for test stability - Update Playwright configuration for better test isolation - Bump required dependencies
- Add onboarding navigation fix documentation - Add Meteor migration scripts - Add password reset E2E test - Add global test setup/teardown utilities - Add onboarding E2E test suite
- Fix AdminTimesheetPanel.tsx: Move fetchData useCallback before DDP useEffect to prevent "Cannot access 'fetchData' before initialization" error. The useEffect was referencing fetchData before its const declaration, causing temporal dead zone violation.
- Fix dashboard test: Add { exact: true } to getByText('Open tickets') to avoid strict mode violation when multiple elements match the selector.
- Fix ticket-timers tests: Add handling for "Clock In Required" dialog that appears when starting a timer without being clocked in. Increased timeouts for real-time sync waits.
- Fix timesheet test: Add waitForLoadState('networkidle') before checking clock state. Implement graceful team-auto-selection wait. Increase test timeout to 60s to accommodate clock-in/out cycle. Wait for button to be enabled before clicking.
All 89 tests now pass (88 passed, 1 flaky that passes on retry).
- Add migrate-all-collections.js for data migration pipeline - Add migrate-all-data.mjs for complete migration workflow - Add migrate-complete.mjs for final migration verification - Add migrate-to-meteor-accounts.js for account conversion - Add set-test-passwords.mjs for test user provisioning - Add verify-user-collections.js for migration validation - Add run-migration.sh as main migration entry point - Add comprehensive migration documentation in docs/ - Move scripts from meteor-backend/scripts to root scripts/ - Add scripts README for migration maintenance
- Add migration-login-handler.js for dual auth system support - Update auth-bridge.js to handle both Better Auth and Meteor users - Update clock.js to support migrated user authentication - Update main.js to register migration login handler - Enable seamless transition from Better Auth to Meteor accounts - Maintain backward compatibility during migration period
- Add ReportIssueModal.test.tsx for feedback component testing - Update ReportIssueModal.tsx to use @mieweb/ui components - Update api.ts to handle Meteor authentication tokens - Update LoginForm.tsx with Meteor login support - Add provision-test-users.mjs for e2e test user setup - Add auth-migration.spec.ts for migration flow testing - Add better-auth-migration.spec.ts for comprehensive auth testing - Update e2e test configuration for migration testing - Update global-setup.ts for test environment preparation - Update ticket-timers.spec.ts for migrated auth - Update README.md with migration instructions - Update .gitignore for migration artifacts - Update package.json dependencies
…wn UX - Add 7 new unit tests for tickets.assign endpoint covering edge cases: - Multiple user assignment - Unassignment (empty array) - Reassignment to different user - Invalid user ID validation - Non-array value rejection - Invalid user ID format rejection - Non-team member rejection - Fix mobile dropdown menu viewport issues on TicketsPage: - Adjust z-index to prevent overlapping with other elements - Add overflow-visible classes for mobile screens - Constrain dropdown max-width on mobile devices - Ensure dropdown content stays within viewport boundaries - Add E2E test for mobile dropdown rendering and positioning - Clean up eslint-disable comments in global-setup and global-teardown - Remove 'Unassigned' option from member filter (assignment now requires explicit user selection)
- Add METEOR_API_BASE for same-origin REST calls through Vite proxy - Add /uploads and /v1 proxy routes to vite.config.ts - Fix teams.getMembers to query both user collections (Meteor + Better Auth) - All mobile REST calls now work in Capacitor WebView - Organizations and member names now display correctly Fixes mobile compatibility with Meteor backend on port 3100.
- Expose pushSubscribe/pushUnsubscribe wormhole methods - Add Meteor methods for device token and web push subscription CRUD - Update nativePush and pushNotificationsClient to route through Meteor API - Add push notification permission status to settings page - Update dev-ios and dev-android scripts - Configure Vite VAPID key env variable
… Apple) - All three OAuth providers now use jose library for JWT signing - Consistent 5-minute token expiration across all providers - JWT includes userId, email, name, provider, and meteorToken claims - Improved security with cryptographically signed tokens - Better frontend compatibility with structured, verifiable tokens
- Remove backend workspace from package-lock.json - Add .gitignore for data/videos and uploads directories - Prepare for Meteor-only deployment
- Remove all Better Auth collection references (session, account, user, verification) - Update auth-bridge.js: Remove session collection lookups, simplify whoami to use only Meteor users - Update main.js: Remove Better Auth fallback lookups in token resolution - Update clock.js: Remove legacy user collection image lookups - Update teams.js: Replace Better Auth account password updates with Meteor Accounts API - Add password reset flow for users without passwords (auto-redirect on login) - Add migration scripts: remigrate-users.js, verify-migration.js, drop-legacy-collections.js - All 42 users migrated, 270 legacy documents dropped - Backup saved at /home/pdharamkar/backups/pre_remigration_2026-07-06T19-16-23/
Resolved conflicts and integrated features from main: - Added privacy.html for App Store requirements - Added user blocking migration (backend/migrations/20260627_120000_add-user-blocked-field.cjs) - Updated .prettierignore to exclude meteor-backend - Improved mobile UI for team join requests - Better error handling in useSession Note: Blocking functionality already exists in meteor-is-back Note: Better Auth migration detection kept for remaining user migrations
- Disable backend checks workflow (Fastify backend removed, Meteor doesn't have lint/typecheck scripts) - Fix 34 frontend lint errors (unused variables/imports) - Remove unused expect imports from page objects - Prefix unused test variables with underscore - Remove unused imports from dashboard tests - Update playwright job to not depend on backend
- Fix MediaItem type in huddle video upload (remove duplicate/invalid fields) - Fix getComments type casting for huddle and comments - Filter out null roles when loading organizations - Fix HuddlePost type casting in Huddle page - Run prettier formatting fix on all files
Fixed ReferenceError issues in 8 failing tests caused by inconsistent variable naming in beforeEach/beforeAll hooks: - tests/e2e/teams/team-invitation.spec.ts: Standardized page object variables to use underscore prefix (_loginPage, _dashboardPage, _teamsPage, _orgPage) - tests/e2e/organizations/blocking-feature.spec.ts: Standardized to use underscore prefix for all variables (_loginPage, _dashboardPage) - tests/e2e/timers/work-summary.spec.ts: Fixed database variable from db to _db All 91 E2E tests now pass successfully.
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.
Meteor-to-Production Plan — Migration Complete
Tracking document for migrating TimeHuddle's backend from Fastify + Better Auth to Meteor 3 +
meteor-wormhole (REST + OpenAPI + MCP from one
method definition), with DDP pub/sub replacing all hand-rolled WebSocket fan-out.
Branch / PR:
meteor-is-back→ PR #357Production URL: https://huddle.os.mieweb.org/
Architecture: Before & After
flowchart TB subgraph Before["❌ Before — Fastify + Better Auth"] direction TB Client1["Frontend\nVite + React 19\nPort 3000"] Fastify["Fastify Backend\nPort 4000"] BetterAuth["Better Auth\nSession + JWT + OAuth"] WS1["WS Hub: Clock"] WS2["WS Hub: Tickets"] WS3["WS Hub: Timers"] WS4["WS Hub: Teams"] WS5["WS Hub: Presence"] WS6["WS Hub: Notifications"] WS7["WS Hub: Messages\n+ Channels"] Mongo1[(MongoDB)] Client1 -->|REST API| Fastify Client1 -->|7 WebSocket connections| WS1 & WS2 & WS3 & WS4 & WS5 & WS6 & WS7 Fastify --> BetterAuth Fastify --> Mongo1 WS1 & WS2 & WS3 & WS4 & WS5 & WS6 & WS7 --> Mongo1 end subgraph After["✅ After — Meteor 3 + Wormhole"] direction TB Client2["Frontend\nVite + React 19\nPort 3000"] Meteor["Meteor 3 Backend\nPort 3100"] Wormhole["meteor-wormhole\nREST + OpenAPI + MCP"] DDP["DDP Protocol\nSingle Connection"] MeteorAccounts["Meteor Accounts\nEmail/Password + OAuth"] Mongo2[(MongoDB)] Client2 -->|"wormholeCall() — REST"| Wormhole Client2 -->|"DDP — Real-time subs"| DDP Wormhole --> Meteor DDP --> Meteor Meteor --> MeteorAccounts Meteor --> Mongo2 end Before -.->|"Migration\n~6 months"| After classDef removed fill:#fee,stroke:#c33,color:#900 classDef added fill:#efe,stroke:#393,color:#060 class Before removed class After addedMigration Principle
Each feature moved as one unit. Fastify kept serving everything not yet moved (shared Mongo, zero
big-bang cutover):
flowchart LR Methods["Port service logic\nto Meteor methods"] --> Pub["DDP publication\nreplaces WS hub"] Pub --> Expose["Wormhole expose\nREST + MCP"] Expose --> Cutover["Frontend cutover"] Cutover --> Delete["Delete Fastify route"]Per-milestone gate:
npm run lint && npm run typecheck && npm run format && npm testgreen,browser smoke test, then commit to PR #357.
What Was Replaced
Authentication — Fastify + Better Auth → Meteor Accounts
POST /api/auth/sign-in/emailaccounts-passwordDDP login handlerPOST /api/auth/sign-up/emailaccounts.createUserMeteor methodaccounts.sendResetPasswordEmail+accounts.resetPasswordMeteor methods (nodemailer)genericOAuthfor Authentik/api/auth/jwks, Fastifyrequire-auth.tsmiddlewareresolveUser()inauth-bridge.js(resume token + PAT)tokens.tsroute, SHA-256 hashedtokens.list/create/revokeMeteor methods, same SHA-256 schemebackend/src/lib/permissions.tsmeteor-backend/server/casl.js— same ability definitionsReal-Time — 7 WebSocket Hubs → DDP Publications
clock-ws.ts— hand-rolled fan-outclock.liveForTeams— oplog-backed cursortickets-ws.tstickets.byTeamtimers-ws.tstimers.liveForUserteams-ws.tsteams.byUserpresence-ws.ts— pollingpresence.watch— in-memory + 75s timeoutnotifications-ws.tsnotifications.liveForUser(viasubscribeNewNotifications)messages-ws.ts+channels-ws.tsmessages.byThread+channelmessages.byChannelResult: 7 separate WebSocket connections replaced by 1 DDP connection with multiplexed subscriptions.
REST API — Fastify Routes → Wormhole Methods
Every Fastify route was replaced by a Meteor method exposed via
meteor-wormhole(auto-generated REST + OpenAPI + MCP):clock.ts(12 endpoints)clock.*(12 methods)wormholeCall()tickets.ts(8 endpoints)tickets.*(8 methods)wormholeCall()timers.ts(12 endpoints)timers.*(12 methods)wormholeCall()teams.ts+teams-admin.ts(12 endpoints)teams.*(12 methods + join-request workflow)wormholeCall()messages.ts(2 endpoints)messages.*(2 methods)wormholeCall()channels.ts(4 endpoints)channels.*(4 methods)wormholeCall()huddle.ts(7 endpoints)huddle.*(7 methods)wormholeCall()+ DDPorg.ts+org-admin.ts(20 endpoints)orgs.*(20 methods)wormholeCall()enterprises.ts(9 endpoints)enterprises.*(9 methods)wormholeCall()users.ts(6 endpoints)users.*(6 methods)wormholeCall()activity.ts(3 endpoints)activity.*(3 methods)wormholeCall()work.ts(1 endpoint)timers.getUserWorkSummarywormholeCall()notifications.ts(6 endpoints)notifications.*(mutations +testPush)wormholeCall()tokens.ts(3 endpoints)tokens.*(3 methods)wormholeCall()presence.ts(1 endpoint)presence.watch(DDP pub)Uploads & Media — Fastify Multipart → Meteor HTTP Handlers
WebApp.connectHandlers(/api/me/avatar,/api/me/background)media.tsmultipart/api/media/upload+media.*methodspulsevault.js) +pulsevault.reserve*methodsattachments.tsattachments.*Meteor methods (metadata-only)WebApp.connectHandlersat/uploads/*Background Jobs — Same Engine, New Host
agendanpm lib in Fastifyagendalib inside Meteoragendajobsin MongoDBagendajobscollectionMETEOR_AGENDA_ENABLED=true)FASTIFY_AGENDA_ENABLED/METEOR_AGENDA_ENABLED)Test Infrastructure
POST /api/auth/sign-in/emailaccounts.createUser+ resume tokensCompleted Phases
Phase 1 — Proof of Concept
meteor-backend/, port 3100, shared Mongo)vendor/meteor-wormhole, mieweb fork)auth.bridgeDDP method + REST token param)tickets.byTeam+clock.liveForTeamssrc/lib/ddp.ts) — dependency-free, EJSON decode, live hooksM0 — Identity & Foundations
AsyncLocalStorage-based context so REST/MCP calls readAuthorizationheader (no credentials in body)requireIdentityfalls back tocurrentBearerToken(),wormholeCall()sendsAuthorization: Bearer/api/auth/jwks, Meteor verified viajosekey cacheemailPasswordlogin handler with native bcrypt verify + Fastify fallback during coexistence,accounts.createUserfor signup, password reset methods (nodemailer)genericOAuth+ OIDC discovery), env-gated server + clientagendalib +agendajobscollection, shift reminders + auto-clockoutM1 — Core Time-Tracking
clock.liveForTeamspub + coupled timer close-outtickets.byTeampub + assignment notifications + activity logtimers.liveForUserpubnotifications.liveForUser) + push notifications (web-push, FCM, APNs) +testPushmethodM2 — Collaboration
presence.watchcustom pub — in-memory tracking, 75s timeout, connection lifecycleactivity.log,activity.userLog,activity.ticketActivity— cursor-paginatedteams.byUserpub + org auto-provisioningmessages.getThread+messages.send+messages.byThreadpub — cursor-paginated, notificationschannelmessages.byChannelpub +#generalauto-provisioning + notification fan-outhuddlePosts.byTeampub + ticket-scoped feedsM3 — Org & Profiles
takeOwnership+installStatus)M4 — HTTP Surfaces & Decommission
/uploads/*/api/media/upload), thumbnail upload, CRUD methods401bad5(June 30, 2026) — entirebackend/directory removedM5 — Post-Merge Drift Reconciliation
clock.createManual+ integration testtimeharbor→timehuddlein connection stringFinal Architecture
Final Stats
backend/removed)timehuddle401bad5)Architecture Decisions
agendanpm lib inside Meteor, sameagendajobscollection — zero migration neededAuthorization: Bearerheader viaAsyncLocalStorageinvocation context — never in the bodyWebApp.connectHandlers(not method-shaped); storage paths unchangedtimeharbor→timehuddle