Own your user store
By default a pool’s users live in AppID’s managed store. Moving them into a K3 bucket makes them your data: query and join them over the pg wire, next to the rest of your app’s tables.
1. Pick (or create) the bucket
dodil data bucket create my-users-bucketAny bucket you own works; AppID creates its tables on first use.
2. Migrate, live
dodil appid user-store migrate my-app bucket:my-users-bucket --create-sa--create-sa provisions the service account AppID uses to reach the bucket (or pass --sa <client-id> for an existing one). The command follows progress:
warm_copy users copied — pool fully live
locked_delta short write-lock — sign-ins answer 503 for the cutover moment
verifying row counts compared
purging managed-store copy removed (skip with --keep-source)
doneA verification mismatch aborts and rolls back automatically. Check anytime with dodil appid user-store status my-app; while migrating, the pool refuses config changes (503).
For a brand-new, empty pool, skip the copy machinery:
dodil appid user-store use-bucket my-app my-users-bucket --create-sa --yes3. Query your users
The pool’s data now lives in these tables in your bucket:
| Table | Holds |
|---|---|
_appid_users | Accounts — id, email, verification, roles, ban state |
_appid_sessions | Refresh-session families |
_appid_email_claims | Outstanding reset/verify links |
_appid_tenants | Tenants |
_appid_tenant_members | Per-tenant memberships and roles |
Query them like any other K3 table:
-- signups per day, last 30 days
SELECT DATE_TRUNC('day', created_at)::DATE AS day, COUNT(*) AS signups
FROM _appid_users
WHERE created_at > CURRENT_TIMESTAMP - INTERVAL 30 DAY
GROUP BY 1 ORDER BY 1;
-- join users against your own app tables
SELECT u.email, o.total
FROM orders o JOIN _appid_users u ON u.id = o.app_user_id
WHERE o.status = 'unpaid';Treat the
_appid_*tables as read-only. AppID owns their writes (hashing, session rotation, tenancy invariants). Write your own tables and join on_appid_users.id. Passwords are argon2id hashes and refresh tokens are stored only as digests — there’s nothing sensitive to leak in plaintext, but keep the bucket private all the same.
Moving elsewhere (or back)
The same machinery migrates in any direction:
dodil appid user-store migrate my-app 'postgres://user:[email protected]/appdb' # your PG
dodil appid user-store migrate my-app '' # back to the managed store
dodil appid user-store migrate-abort my-app --yes # bail out mid-flightThe pool’s record never stores your credentials — a K3 store is an address (k3+sa://…) for which AppID mints short-lived tokens on demand; a BYO-Postgres DSN is held encrypted in the control plane.