Repository navigation
feat(notifications): send Android alerts from paired servers - #41
Merged
Merged
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Startup catch-up can miss alerts, failed preference persistence can orphan registrations, and the mobile synchronization lifecycle lacks focused tests.
3 open findings
What changed in this PR
Adds privacy-minimized Android notifications sent directly by paired servers, independent of T3 Connect.
Changes:
- Adds authenticated registration, delivery, retry, and notification-resolution services.
- Adds Android settings, token synchronization, native presentation, and tap routing.
- Extracts shared FCM infrastructure and documents deployment/privacy requirements.
| File | Description |
|---|---|
wiki/paired-notifications.md |
Records compatibility and privacy decisions. |
wiki/log.md |
Logs the wiki addition. |
wiki/index.md |
Links the new decision page. |
packages/shared/src/FcmClient.ts |
Generalizes the shared FCM sender. |
packages/shared/src/FcmAssertionSigner.ts |
Generalizes FCM assertion signing. |
packages/shared/src/agentAwareness.ts |
Exports phase resolution. |
packages/shared/package.json |
Exposes shared FCM modules. |
packages/contracts/src/environmentHttp.ts |
Defines notification schemas and endpoints. |
packages/client-runtime/src/rpc/pairedNotifications.ts |
Adds notification HTTP clients. |
packages/client-runtime/src/rpc/index.ts |
Exports notification RPC helpers. |
infra/relay/src/worker.ts |
Uses shared FCM services. |
infra/relay/src/WebCrypto.ts |
Removes the relay-local crypto service. |
infra/relay/src/agentActivity/FcmDeliveries.ts |
Uses the shared FCM client. |
infra/relay/src/agentActivity/FcmDeliveries.test.ts |
Updates the FCM test mock. |
infra/relay/src/agentActivity/FcmClient.test.ts |
Tests the extracted FCM modules. |
infra/relay/scripts/android-push-watch.ts |
Migrates the watch utility to shared FCM. |
infra/relay/scripts/android-push-smoke.ts |
Migrates the smoke utility to shared FCM. |
fnox.toml |
Adds encrypted Firebase settings. |
docs/user/mobile-notifications.md |
Documents paired Android alerts. |
docs/operations/android-notifications.md |
Documents deployment and credentials. |
apps/server/src/server.ts |
Wires notification runtime and routes. |
apps/server/src/notifications/runtime.ts |
Builds and starts notification services. |
apps/server/src/notifications/PairedNotifications.ts |
Implements authorization and delivery. |
apps/server/src/notifications/PairedNotifications.test.ts |
Tests server notification behavior. |
apps/server/src/notifications/NotificationStore.ts |
Persists registrations and deliveries. |
apps/server/src/notifications/http.ts |
Implements authenticated HTTP handlers. |
apps/mobile/src/Stack.tsx |
Adds synchronization and tap routing. |
apps/mobile/src/persistence/mobile-preferences.ts |
Persists per-environment preferences. |
apps/mobile/src/features/settings/SettingsRouteScreen.tsx |
Exposes notification settings. |
apps/mobile/src/features/settings/SettingsNotificationsRouteScreen.tsx |
Integrates paired settings. |
apps/mobile/src/features/settings/PairedNotificationSettings.tsx |
Implements paired notification toggles. |
apps/mobile/src/features/agent-awareness/pairedNotifications.ts |
Synchronizes registrations and tokens. |
apps/mobile/src/features/agent-awareness/PairedNotificationRouteScreen.tsx |
Resolves notification destinations. |
apps/mobile/src/features/agent-awareness/androidNotifications.ts |
Bridges paired native configuration. |
apps/mobile/modules/t3-agent-notifications/android/src/test/java/expo/modules/t3agentnotifications/AgentNotificationsTest.kt |
Tests native paired alerts. |
apps/mobile/modules/t3-agent-notifications/android/src/main/java/expo/modules/t3agentnotifications/T3AgentNotificationsModule.kt |
Exposes native paired configuration. |
apps/mobile/modules/t3-agent-notifications/android/src/main/java/expo/modules/t3agentnotifications/AgentNotifications.kt |
Handles and presents paired FCM alerts. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+42
to
+50
| if (enabled) await runtime.runPromise(enablePairedNotifications(connection, registrationId)); | ||
| await save({ | ||
| transform: (current) => ({ | ||
| pairedNotifications: { | ||
| ...current.pairedNotifications, | ||
| [connection.environmentId]: { registrationId, enabled }, | ||
| }, | ||
| }), | ||
| }); |
Comment on lines
+308
to
+312
| (catchUp | ||
| ? completedAt !== undefined && | ||
| now - completedAt < COMPLETION_ALERT_TTL_MS && | ||
| previous !== undefined | ||
| : previous !== undefined || (completedAt !== undefined && completedAt >= startedAt))); |
Comment on lines
+137
to
+141
| export function usePairedNotificationSync(): void { | ||
| const preferences = useAtomValue(mobilePreferencesAtom); | ||
| const { savedConnectionsById, isLoadingSavedConnection } = useSavedRemoteConnections(); | ||
| const previousConnections = useRef<Readonly<Record<string, SavedRemoteConnection>>>({}); | ||
| useEffect(() => { |
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
Self-hosted Android notifications previously required T3 Connect, even when the phone was already paired directly with its server. Nicholas needs completion, failure, approval, and input alerts without sending conversation content through Connect.
Let each paired server send generic alerts directly through Firebase Cloud Messaging using the phone's existing authenticated session. Google receives opaque registration/notification IDs, the event phase, and a timestamp—not thread titles, prompts, paths, server addresses, or thread destinations. Notification taps resolve the destination through the paired server.
Persist registrations and retryable deliveries in the existing private file store, recheck session authorization and current thread state before sending, and handle token rotation, invalid tokens, opt-out, and revocation. Android settings and native presentation work independently of Connect sign-in. Reuse the FCM sender across paired and hosted delivery; ongoing cards and iOS push remain on the existing Connect path. No new account system, hosting service, dependency, or database migration.
Include the two authorized Firebase settings in the existing age-encrypted fnox local profile. The Google services file stays gitignored in the main checkout; service-account private keys are not committed in plaintext or bundled into the phone app.
Test plan
Implementation verification earlier in this thread: 86 focused TypeScript tests, 132 native Android tests, and Android lint passed.
Scoped server, mobile, and relay typechecks; targeted formatting and lint passed. Existing React lint warnings remain.
Firebase project/package compatibility and sender permissions checked; an FCM validate-only request returned HTTP 200 without sending a notification.
Replacement credential saved encrypted in fnox and verified without printing secret values; neither Firebase private key appears in the staged diff.
Wiki frontmatter/local links, main-checkout Google services gitignore coverage, and diff whitespace checks passed.
After merging, built the macOS arm64 Alpha app from the normal local checkout with Node 24.13.1. Verified the installed app link and embedded commit
7fa6e59d3d71. The build succeeded with dependency-install warnings about missing vite/vitest bin targets.Integrated mobile verification and before/after screenshots: Device access is disabled for this environment.
Physical-phone background delivery, cold-start taps, offline retry, opt-out, and revocation after installing the new signed APK and updating its sending server.
Before + After
Before: directly paired Android environments could not send push alerts without T3 Connect.
After: Settings → Notifications → Paired environments offers independent per-environment alerts, with generic notification text and authenticated opaque tap routing.
Screenshots could not be captured because Device access is disabled for this environment.
Rollout note
Nicholas requested opening and merging this PR, updating
/Users/nicholas/Documents/Projects/t3code, moving this session there, and rebuilding the macOS arm64 Alpha app. Leave the running app and remote servers untouched; Nicholas will restart the desktop app after the local rebuild is ready.Enabling live phone delivery is a separate rollout: build/install a bundled preview APK with the matching Google services configuration, the existing private signing identity, and
T3CODE_MOBILE_UPDATES_ENABLED=0; configureT3CODE_FCM_SERVICE_ACCOUNTon each sending host at server startup; update/restart that server; then enable the paired environment on the phone. Local fnox settings do not automatically reach an SSH-managed server, and a build-time secret alone does not configure its runtime.Merged at
7fa6e59d3d71f4b7d12c8c40cab89e033a1f9537while GitHub CI was queued. The local checkout and this session are on updatedmain, the Alpha app has been rebuilt, and the running app and remote servers were not restarted. GitHub CI remains pending; the signed Android APK and sending-server activation have not been performed.brave-tulip-laurel