platform_users table records membership: which users belong to which
platform (tenant).
No mutation API for memberships. Rows are written in only two ways:
POST /auth/token— automatically, on every login. See below.POST /platform_users/backfill— admin-only, one-shot, run once per environment after deploy.
GET /platforms and GET /users/details/basic
also read the table:
GET /platform_users?user_id=…&platform_id=…&statuses=… — a flat list of
raw rows, newest-first by default.The relationship is many-to-many
A user is keyed by their phone number, and the same phone can sign in through more than one partner platform. So a user holds one membership row per platform, not a single platform assignment. This is also why the tenant is not a column onusers. A scalar
users.platform_id would be ambiguous the moment a phone appeared on a second
platform, and the second signup would silently overwrite the first.
How memberships are created
POST /auth/token is the only path a user enters the system, and the only
place the platform is authoritative — it comes from the calling platform’s
service-account JWT claim, never from a request body, so it cannot be spoofed.
1
A platform mints an app token
The partner backend calls
POST /auth/token with its service-account
bearer token and the user’s phone number.2
The user is found or created
Existing phone → the existing user. New phone → a fresh
ONBOARDING
user.3
The membership is recorded
The
(user, platform) pair is upserted. Idempotent — every subsequent
login through the same platform is a no-op, and a login through a
different platform simply adds a second membership.Reading memberships
GET /platform_users returns a flat, filtered page of platform_users
rows. Every filter is optional and composes with AND.
All rows (paginated)
Filter by platform + status
Look up one pair
Calling with a non-admin token
The token wins over the query: a pinned filter takes the token’s value, whatever the query says.statuses, limit and offset behave exactly as for admins.
Contributor — the beneficiary's membership on the token's platform
Backfilling historical memberships
POST /platform_users/backfill is a one-shot admin endpoint. The
migration ships schema only; this endpoint fills in the (user, platform)
pairs that predate the table.
Run it with dry_run first to see exactly what it would do, then for real:
Dry run
Response
results lists only the users
that were not linked:
Two steps:
Re-runnable. A user–platform pair that already has a membership row (even a revoked one) is never a candidate, so once everything
is linked a rerun reports
succeeded: 0, and a run interleaved with logins
covers only the pairs login has not linked yet. A user whose ledger insert
failed is not pushed onto the fallback platform; the next run retries it.
created_at is copied from the source row, so it records when the membership
began rather than when the backfill ran. A revoked pair is left revoked.
Revoking a membership
There is no API for this. Thestatus column exists so an operator can
revoke a membership directly in the database. Each (user, platform) pair has
exactly one row, so a revoked membership is flipped back to active in place on
the user’s next login through that platform.