@glydi/passkey-store-prisma
v0.1.0
Published
First-party Prisma/Postgres adapter for Glide passkeys. Schema exposed as data — consumers paste the .prisma fragment and run their own prisma migrate.
Readme
@glydi/passkey-store-prisma
First-party Prisma/Postgres adapter for Glide passkeys. Provides a conformant GlideStore backed by two PostgreSQL tables — schema exposed as data via a .prisma fragment, zero numbered .sql files.
Quick Start
import { PrismaClient } from "@prisma/client";
import { createPrismaStore } from "@glydi/passkey-store-prisma";
import { createGlideServerFromEnv } from "@glydi/passkey-server";
const prisma = new PrismaClient();
const store = createPrismaStore({
prisma,
usersTable: "users",
userIdColumn: "id",
usernameColumn: "email",
});
const glide = createGlideServerFromEnv({ store });All four opts are required — the adapter validates them at startup with clear [Glide] errors.
createGlideServerFromEnvdefaults:userVerificationdefaults to"preferred". SetGLIDE_USER_VERIFICATION=requiredin your environment for stricter behavior (recommended for production).GLIDE_RP_NAMEis required — set it to your app's display name (e.g."My App").
Consumer-Owns-Migrations
Paste the following models from PRISMA_SCHEMA (exported from @glydi/passkey-store-prisma/schema) into your own schema.prisma, alongside your User model:
model PasskeyCredential {
credentialId String @id @map("credential_id")
userId String @map("user_id")
publicKey Bytes @map("public_key")
counter BigInt @default(0)
transports String?
deviceType String? @map("device_type")
backedUp Int? @map("backed_up")
name String?
createdAt BigInt? @map("created_at")
// Replace "User" with your actual user model name.
// Add passkeys PasskeyCredential[] back-reference on your User model.
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@map("passkey_credentials")
}
model PasskeyChallenge {
sessionId String @id @map("session_id")
challenge String
expires BigInt // unix milliseconds — BIGINT avoids INT4 overflow of Date.now()
@@map("passkey_challenges")
}IMPORTANT — back-reference required: Add a
passkeys PasskeyCredential[]field to yourUsermodel. Prisma validates referential integrity in the schema DSL even for raw-SQL-only adapters; without the back-reference,prisma validate(andprisma migrate dev) will fail.
Then run your own migration:
npx prisma migrate dev --name add_passkey_tablesYou own your data, so you own your migrations. This adapter ships no numbered .sql files — avoiding the 0013 collision problem seen when adapters ship their own numbered files that overlap with a consumer's migration counter.
How the Adapter Works
The adapter talks to the database entirely via parameterized raw SQL (prisma.$queryRaw / prisma.$executeRaw). It is independent of your generated client's model/delegate names — a published generic adapter cannot depend on consumer-specific generated models.
This means:
- You pass
usersTable,userIdColumn, andusernameColumnas plain strings (not Prisma model objects) - The adapter never calls
prisma.passkeyCredential.*orprisma.passkeyChallenge.* - The
.prismaschema fragment above is used only to drive yourprisma migrateDDL generation, not by the adapter itself
usernameColumn UNIQUE Constraint
For the concurrency-resilient upsert to work correctly, usernameColumn (e.g. email) should have a UNIQUE constraint in your database. Without it, a race between two simultaneous registrations for the same new username could silently create duplicate rows.
In Prisma schema terms:
model User {
id String @id
email String @unique // <-- needed for safe upsert
passkeys PasskeyCredential[]
}Schema Changelog
SCHEMA_VERSION is exported from @glydi/passkey-store-prisma/schema so your app can assert compatibility at startup.
v2
Columns widened in passkey_credentials:
| Column | v1 Type (SQL / Prisma) | v2 Type (SQL / Prisma) | Reason |
|--------|------------------------|------------------------|--------|
| counter | INTEGER / Int | BIGINT / BigInt | WebAuthn signature counters are uint32 (max 4,294,967,295), which exceeds PostgreSQL INT4 max of 2,147,483,647 |
| created_at | INTEGER / Int? | BIGINT / BigInt? | Stored as unix milliseconds; Date.now() ≈ 1.75 × 10¹² overflows INT4 |
Migration for existing deployments: Regenerate from PRISMA_SCHEMA (update your schema.prisma with the BigInt types below) and apply:
ALTER TABLE passkey_credentials
ALTER COLUMN counter TYPE BIGINT,
ALTER COLUMN created_at TYPE BIGINT;This is a safe, backward-compatible widening — no data is lost and no application code changes are required.
SCHEMA_VERSION exported from @glydi/passkey-store-prisma/schema is now 2. You can assert compatibility at startup:
import { SCHEMA_VERSION } from "@glydi/passkey-store-prisma/schema";
assert(SCHEMA_VERSION === 2, "passkey schema version mismatch");v1
Tables introduced: passkey_credentials, passkey_challenges
passkey_credentials
| Column | Type | Notes |
|--------|------|-------|
| credential_id | TEXT PRIMARY KEY | WebAuthn credential ID (base64url) |
| user_id | TEXT NOT NULL | FK → your users table id column |
| public_key | BYTEA NOT NULL | Raw COSE public key bytes |
| counter | BIGINT NOT NULL DEFAULT 0 | Signature counter (replay protection) |
| transports | TEXT | JSON-serialized array of AuthenticatorTransport |
| device_type | TEXT | "singleDevice" or "multiDevice" |
| backed_up | INTEGER | 1 = backed up, 0 = not, NULL = unknown |
| name | TEXT | Consumer-supplied display name |
| created_at | BIGINT | Unix milliseconds timestamp |
Index: idx_passkey_credentials_user on user_id.
FK: fk_passkey_credentials_user → userIdColumn argument passed to createPrismaStore().
passkey_challenges
| Column | Type | Notes |
|--------|------|-------|
| session_id | TEXT PRIMARY KEY | Challenge session identifier |
| challenge | TEXT NOT NULL | WebAuthn challenge (base64url) |
| expires | BIGINT NOT NULL | Unix milliseconds — BIGINT avoids INT4 overflow of Date.now() |
expires is stored as unix milliseconds. BIGINT (not INTEGER) is required because Date.now() returns ~1.7 trillion ms, which exceeds PostgreSQL INT4's maximum of ~2.1 billion. The adapter uses lazy TTL eviction: expired challenges are deleted on read, no background sweeper.
Upgrade notes: No prior version exists. Fresh install — paste the models above into your schema.prisma and run prisma migrate dev after adding them.
