Users and data
How Ploy Auth stores users, hashes passwords, and how to reference users from your own data.
Users and data
Ploy Auth keeps users in a regular Ploy database in your organization, so you can inspect, query, back up, and export them like any other data.
Where users live
auth.binding in ploy.yaml names the database. AUTH_DB resolves to the organization database auth_db, created on first deploy if it doesn't exist. The same database is bound to your worker as env.AUTH_DB.
| Table | Contents |
|---|---|
ploy_users | One row per user: id, email (unique, lowercase), email_verified, password_hash, metadata (JSON), created_at, updated_at |
ploy_sessions | One row per session: user_id, SHA-256 token_hash, expires_at, revoked |
ploy_identities | Linked Google, Apple, and GitHub accounts: user_id, provider, provider_user_id, email, email_verified, profile (name and avatar) |
ploy_passkeys | Registered passkeys: user_id, credential id, public_key, signature counter, transports, name, last_used_at |
password_hash is an empty string for users who only use social sign-in or passkeys. Sessions, identities, and passkeys reference ploy_users(id) with ON DELETE CASCADE, so deleting a user removes everything attached to them.
Ploy creates and upgrades these tables itself. Treat them as read-only from your code and use the endpoints to change accounts.
Databases are scoped to the organization by name. Two projects that both use
auth.binding: AUTH_DB share the auth_db database and therefore the same
users. Sessions stay separate: each project signs sessions with its own
secret. Use a different binding name per project, e.g. SHOP_AUTH, for
separate user bases.
Password hashing
New passwords are hashed with scrypt (N = 214, r = 8, p = 5, 16-byte random salt, 64-byte key), one of the OWASP-recommended configurations. The stored value records its parameters:
scrypt$16384$8$5$<salt, base64url>$<hash, base64url>Accounts created before scrypt use PBKDF2-HMAC-SHA512 with 100,000 iterations (<salt>:<hash>). They keep working and are rehashed with scrypt on the user's next successful sign-in. The same upgrade path applies if the parameters are raised later.
Passwords must be 8 to 128 characters. Unknown emails get the same response and the same hashing cost as a wrong password.
Referencing users from your data
Use ploy_users.id as the user key in your own tables. Treat it as an opaque string: older accounts have short IDs and newer ones UUIDs.
Keep your tables in a separate database binding. Ploy applies your migrations to db bindings, never to the auth database:
kind: dynamic
build: pnpm build
out: dist
auth:
binding: AUTH_DB
db:
DB: appCREATE TABLE posts (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX posts_user_id ON posts(user_id);The tables live in different databases, so there is no SQL foreign key between them. Resolve the user from the session, then query with the ID:
const user = await env.PLOY_AUTH.getUser(token);
if (!user) {
return new Response("Unauthorized", { status: 401 });
}
const { results } = await env.DB.prepare(
"SELECT * FROM posts WHERE user_id = ? ORDER BY created_at DESC",
)
.bind(user.id)
.all();For admin views that need user details next to your data, read ploy_users through env.AUTH_DB and merge in code. Store app-specific profile data in your own tables keyed by user_id, or in metadata for a few small fields set at sign-up.
When you delete a user, also delete or anonymize their rows in your own database; Ploy only cascades inside the auth database.
Deployments and environments
- Every deployment of a project shares the same users. Auth settings and the auth database belong to the project, so preview and branch deployments sign in against production users.
- Settings apply instantly to all deployments; they are not part of a deploy.
- Your migrations run on production deploys of the default branch, after Ploy has created the auth tables.
- Local development with
ploy devstores users in a local SQLite database, separate from production. Locally, read users throughPLOY_AUTHor/_ploy/auth/merather thanenv.AUTH_DB.
How is this guide?
Last updated on