Ploy
Ploy
Authentication

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.

TableContents
ploy_usersOne row per user: id, email (unique, lowercase), email_verified, password_hash, metadata (JSON), created_at, updated_at
ploy_sessionsOne row per session: user_id, SHA-256 token_hash, expires_at, revoked
ploy_identitiesLinked Google, Apple, and GitHub accounts: user_id, provider, provider_user_id, email, email_verified, profile (name and avatar)
ploy_passkeysRegistered 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:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist
auth:
  binding: AUTH_DB
db:
  DB: app
migrations/001_create_posts.sql
CREATE 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 dev stores users in a local SQLite database, separate from production. Locally, read users through PLOY_AUTH or /_ploy/auth/me rather than env.AUTH_DB.

How is this guide?

Last updated on