Skip to content

feat(notifications): send Android alerts from paired servers - #41

Merged
NicholasZolton merged 1 commit into
mainfrom
NicholasZolton/03e4dcb3
Oct 10, 2026
Merged

NicholasZolton merged 1 commit into
mainfrom
NicholasZolton/03e4dcb3

Conversation

@NicholasZolton

@NicholasZolton NicholasZolton commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

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; configure T3CODE_FCM_SERVICE_ACCOUNT on 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 7fa6e59d3d71f4b7d12c8c40cab89e033a1f9537 while GitHub CI was queued. The local checkout and this session are on updated main, 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

Copilot AI balanced review requested due to automatic review settings October 10, 2026 17:57
@NicholasZolton
NicholasZolton merged commit 7fa6e59 into main Oct 10, 2026
8 of 23 checks passed
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Oct 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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(() => {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants