Skip to Content
We are live but in Staging 🎉
RecipesOwn Your User Store

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-bucket

Any 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) done

A 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 --yes

3. Query your users

The pool’s data now lives in these tables in your bucket:

TableHolds
_appid_usersAccounts — id, email, verification, roles, ban state
_appid_sessionsRefresh-session families
_appid_email_claimsOutstanding reset/verify links
_appid_tenantsTenants
_appid_tenant_membersPer-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-flight

The 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.