[Spike] Notifications #1677
davidgamez
started this conversation in
Decisions
Replies: 1 comment
-
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Notifications — Implementation Proposal
Related issues: #196 · #197
1. User Data Storage
Decision: migrate to PostgreSQL, isolated schema, synced via a
GET /v1/users/meupsert.Currently user records live in Firebase Firestore. The UI (
mobilitydatabase-web) retrieves and writes them through two Firebase Callable Functions defined in thefunctions/folder of that repo:retrieveUserInformationauth-saga.ts— on every login and OAuth provider sign-in{ fullName, organization, isRegisteredToReceiveAPIAnnouncements }from FirestoreupdateUserInformationprofile-saga.ts— on profile saveAuthentication continues to use Firebase/Google OAuth2 (the token flow in
functions-python/tokens/andRequestContextMiddlewareremain unchanged — they already extractuser_id(Firebase UID) anduser_emailfrom the Bearer/IAP JWT).User data (profile, org membership, subscriptions) moves to PostgreSQL under a dedicated
usersschema so it cannot be inadvertently joined against feed data and PII stays compartmentalised.Sync with the sign-up / login process
The
GET /v1/users/meendpoint performs an upsert on every call: if noapp_userrow exists for the Firebase UID, it is created automatically. This means:signUpSaga): after FirebasecreateUserWithEmailAndPassword, a call toGET /v1/users/meprovisions the row. No separate registration step is needed.emailLoginSaga,loginWithProviderSaga): same call replaces the currentretrieveUserInformationFirebase callable.tasks_executortask (see §1.1 below). Any user not yet migrated is provisioned on demand at next login via the upsert.1.1 Firebase migration task (
tasks_executor)Rather than a one-shot backfill script, the migration runs as a repeatable task inside the existing
tasks_executorCloud Function. This handles the deployment gap: the UI and backend may be deployed independently, so new Firestore users can appear after the first run.2. OpenAPI Schema
Yes — one new schema file following the established pattern.
Every API in this repo is spec-first:
docs/DatabaseCatalogAPI.yamlscripts/api-gen.shapi/src/feeds_gen/api/)docs/OperationsAPI.yamlscripts/api-operations-gen.shfunctions-python/operations_api/src/feeds_gen/The user service follows the same pattern:
docs/UserServiceAPI.yaml(new)scripts/api-user-service-gen.sh(new)functions-python/user_service/src/user_service_gen/One schema, not two. Users and notifications are tightly coupled (subscriptions reference users, orgs fan out to users). A single
UserServiceAPI.yamlavoids cross-file$refcomplexity. If the service grows significantly it can be split later.Top-level structure of
docs/UserServiceAPI.yamlAuthorization
All user, notification, and organization endpoint results are scoped per user. A user can only fetch organizations linked to their profile; the same applies to notifications.
3. Modularity
Decision: new Cloud Function / FastAPI module inside this repo.
A completely separate repository would duplicate the shared libraries (
shared/, auth middleware, DB session helpers) and complicate deployment. Instead, add afunctions-python/user_service/module following the same pattern asoperations_api.Separate
database_genper serviceThe existing
db-gen.shgeneratesapi/src/shared/database_gen/sqlacodegen_models.pyfromsearch_path=public— it only contains feed/dataset models. Theuser_serviceuses a different PostgreSQL schema (users.*) so it needs its own generated models:publicusersscripts/db-gen.shscripts/db-user-service-gen.sh(new)api/src/shared/database_gen/api/src/shared/user_database_gen/(new)function_config.json→"include_api_folders": ["database_gen", ...]function_config.json→"include_api_folders": ["user_database_gen", ...]db-user-service-gen.shrunssqlacodegenwithsearch_path=usersand writes touser_database_gen/. The user service imports fromshared.user_database_gen.sqlacodegen_models— it never touches feed models and vice versa.The
RequestContextMiddlewarealready providesuser_emailfrom the validated OAuth2 token, so no new auth mechanism is needed.4. Organization Entity (Post-MVP)
Decision: organization as a first-class entity with role-based membership; any authenticated user can create one.
memberadminadminrole within their orgplatform_adminA user can belong to multiple organizations. Any authenticated user can create a new organization (they become its first
admin). Org admins manage their own org; platform admins override everything.Role assignment endpoint (
PATCH /v1/organizations/{id}/members/{user_id}): accepts{ "role": "admin" | "member" | "platform_admin" }. Org admins can promote/demote within their org up toadmin; onlyplatform_admincan assign theplatform_adminrole.Organization flow during sign-up / registration — with UI integration
Currently
organizationis a free-text string field onUserDatain the UI (src/app/types.ts). After migration the registration/profile form makes organization optional and introduces a join-request model:POST /v1/organizations— user becomesadminof the new orgPOST /v1/organizations/{id}/join-requests— request ispendinguntil an org admin approvesJoin requests are visible to org admins via
GET /v1/organizations/{id}/join-requests?status=pending. When accepted, anorganization_membershiprow is inserted. When rejected the request is marked rejected and no membership is created.Organization data migration from Firestore (
tasks_executor)The Firestore user records contain
organizationas a free-text string with no deduplication. The migration requires:migrate_firebase_userstask (§1.1) writes the raw string intoapp_user.legacy_org_name.cluster_legacy_organizations) reads all distinctlegacy_org_namevalues and groups them by fuzzy similarity usingrapidfuzz(already available in the Python ecosystem). It writes candidate groups to a staging table (users.org_merge_candidate) for review.platform_adminreviews the candidate groups in the UI and decides which strings map to the same real-world organization.POST /v1/organizations/{id}/mergeto collapse duplicates, orPOST /v1/organizationsto create new entities for un-matched names.assign_legacy_org_memberships) readslegacy_org_name, resolves the now-canonicalorg_id, and insertsorganization_membershiprows (role:member).legacy_org_namecolumn once all users are linked.Staging table for review:
5. Notifications Design
Predefined notifications (MVP-TBD)
System-defined events that users can subscribe to
Custom notifications (TBD PostPost MVP)
A user defines a saved search query (using the same filter parameters already exposed by
/v1/feeds). The system re-runs the query daily and notifies the user when new results appear since the last run.Delivery
member+ users of that org.6. Notification Engine
Recommendation: Cloud Scheduler + existing Python Cloud Function pattern.
No new framework is needed. A scheduled Cloud Function (
notification_dispatcher) runs daily:notification_subscriptionfor due subscriptions.last_notified_at.notification_log.This mirrors the existing
batch_datasets/tasks_executorpattern and requires no new scheduler infrastructure.7. Proposed SQL Entities
All tables live in a
usersschema. Tables are labeled MVP or Post-MVP.8. UI Migration (
mobilitydatabase-web)What changes in
profile-service.tsretrieveUserInformation(Firebase callable)GET /v1/users/me— returns{ id, firebase_uid, email, full_name, organization }updateUserInformation(Firebase callable)PUT /v1/users/me— body{ full_name, is_registered_to_receive_api_announcements }organization: string(free text)organization_id: stringresolved via org search/create flow (see §3)Where the calls happen (saga changes)
auth-saga.ts—emailLoginSagaandloginWithProviderSaga: replaceretrieveUserInformationwithGET /v1/users/me. The upsert ensures the row exists;isRegisteredis derived from whetherfull_nameis set (same semantics as today).auth-saga.ts—signUpSaga: add aGET /v1/users/mecall aftercreateUserWithEmailAndPasswordto provision the row immediately.profile-saga.ts—refreshUserInformation: replaceupdateUserInformationwithPUT /v1/users/me.Authentication header
The UI already attaches the Firebase ID token as a Bearer header via
getUserAccessToken()inapi-auth-middleware.ts. The newuser_serviceendpoints use the sameRequestContextMiddleware— no new auth wiring needed.9. Feature Flags
Organization features are post-MVP but share the same codebase. Feature flags decouple deployment from release: org-related endpoints and UI flows are deployed but inactive until the flag is enabled per environment.
Current system (Firebase Remote Config)
The web already has a feature flag system backed by Firebase Remote Config (
src/lib/remote-config.server.ts). Key characteristics:src/app/interface/RemoteConfig.tsas a typedRemoteConfigValuesinterface.@mobilitydata.orgusers get all boolean flags forced totruevia thefeatureFlagBypassemail-regex config.enableMetrics,enableLanguageToggle,enableFeedStatusBadge,gbfsValidator,gtfsFeatureTracker, etc.This system is not replaced — new SQL-based flags run in parallel until a full migration is complete.
New approach: SQL-based flags
SQL flags are more dynamic: no Firebase console access required, no deploy or cache flush needed, and they are queryable alongside user/subscription data.
SQL tables (in the
usersschema):MVP — global flag definition + per-user:
Post-MVP — org-level overrides:
Resolution logic (MVP: user → global; post-MVP: user → org → global):
10. Proposed Follow-up Issues
MVP
docs/UserServiceAPI.yaml+ gen scriptsscripts/api-user-service-gen.sh,scripts/gen-user-service-config.yaml, andscripts/db-user-service-gen.sh(sqlacodegensearch_path=users→api/src/shared/user_database_gen/).usersschema Liquibase migration (MVP tables)app_user(withlegacy_org_name),notification_type,notification_subscription,notification_log.migrate_firebase_userstask intasks_executorusers.app_user(skip existing byfirebase_uid), stores raw org string inlegacy_org_name. Supportsdry_run. Safe to run repeatedly during deployment window.GET/PUT /v1/users/me— profile API with upsertGETupsertsapp_useron first call (replacesretrieveUserInformationFirebase callable).PUTupdates profile (replacesupdateUserInformation).isRegisteredderived fromfull_name != null.mobilitydatabase-web: replaceretrieveUserInformation/updateUserInformationFirebase callables with REST calls to/v1/users/me. UpdatesignUpSagato call the new API. TBDOrganization field removed from registration form for MVP.notification_typetable; agree on the initial set of events (feed published, status changed, dataset updated, validation report ready).POST/DELETE /v1/notifications/subscriptions; user-level only for MVP.BREVO_API_KEYto Secret Manager; wire into the dispatcher function.users.feature_flag+users.user_feature_flagtables to Liquibase migration (MVP). Addapi/src/utils/feature_flags.pywithis_enabled(user_id)resolution. Addfeaturesobject toUserProfileresponse inUserServiceAPI.yaml. Update web to merge SQL flags intoRemoteConfigValues.Post-MVP
usersschema Liquibase migration (org tables)organization,organization_membership,organization_join_request,org_merge_candidate. Addorg_idcolumn tonotification_subscription.GET /v1/organizations?name=(search),POST /v1/organizations(create + auto-admin), member management, role assignment,POST/GET/PATCH /v1/organizations/{id}/join-requests. Organization optional at registration.POST /v1/organizations/{id}/mergeendpointplatform_adminonly. Reassigns all memberships and subscriptions from source orgs to the target, then deletes sources.cluster_legacy_organizationstask: fuzzy-clusterslegacy_org_namewithrapidfuzz→org_merge_candidate. Human review byplatform_admin.assign_legacy_org_membershipstask finalizes links. Droplegacy_org_nameonce complete.org_idsubscriptions to individual member emails.users.org_feature_flagand update resolution to user → org → global.Beta Was this translation helpful? Give feedback.
All reactions