Background
Supabase announced passkey authentication on 2026-05-28 (beta). After researching their implementation and auditing our codebase, Constructive already has a more complete WebAuthn foundation — we just need to wire up the HTTP layer.
Current State
✅ Database Layer (Ready)
Schema (constructive-db):
webauthn_credentials table with full spec support:
credential_id (Base64url, globally unique)
public_key (COSE-encoded)
sign_count (clone detection)
webauthn_user_id (privacy-preserving handle)
transports[] (usb, nfc, ble, internal, hybrid)
credential_device_type (singleDevice/multiDevice)
backup_eligible, backup_state (sync passkey tracking)
name (user label)
last_used_at
Configuration (webauthn_auth_module):
rp_id, rp_name
origin_allowlist[] (unlimited, vs Supabase's 5-origin limit)
attestation_type
require_user_verification
resident_key
challenge_expiry
✅ Stored Procedures (Ready)
Located in constructive-db/packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/webauthn_auth/:
| Procedure |
Signature |
Purpose |
webauthn_begin_registration |
(p_user_id uuid) → jsonb |
Generate registration challenge, store in session_secrets |
webauthn_finish_registration |
(p_credential_id text, p_public_key bytea, p_sign_count bigint, p_transports text[], p_credential_device_type text, p_backup_eligible boolean, p_backup_state boolean, p_webauthn_user_id text, p_user_id uuid, p_name text) → uuid |
Verify challenge consumed, store credential |
webauthn_begin_sign_in |
(p_user_id uuid DEFAULT NULL) → jsonb |
Generate auth challenge (supports usernameless/discoverable) |
webauthn_finish_sign_in |
(p_credential_id text, p_new_sign_count bigint, p_new_backup_state boolean, credential_kind text) → (user_id uuid, access_token text, access_token_expires_at timestamptz) |
Verify assertion, update signCount, mint session token |
Key implementation details:
- Challenge stored in
session_secrets table, scoped to jwt_private.current_session_id()
- Challenge consumed atomically via
DELETE ... RETURNING
- All procedures are
SECURITY DEFINER in private schema (not exposed to authenticated role)
finish_sign_in returns same signature as sign_in_identity/sign_in_magic_link for consistent downstream handling
✅ ORM Models (Ready)
WebauthnCredentialModel - full CRUD
WebauthnAuthModuleModel - config management
WebauthnSettingModel - per-schema settings
⚠️ GraphQL Server (Partial)
webauthnSettings loaded into API context ✅
- Missing: HTTP routes to orchestrate the WebAuthn ceremony
What's Missing
1. HTTP API Endpoints
POST /auth/webauthn/register/begin
POST /auth/webauthn/register/finish
POST /auth/webauthn/sign-in/begin
POST /auth/webauthn/sign-in/finish
2. WebAuthn Verification Layer
Need @simplewebauthn/server to:
- Generate registration/authentication options (using challenge from DB procedure)
- Verify attestation responses (register) — extract
credentialId, publicKey, signCount, etc.
- Verify assertion responses (sign-in) — validate signature, extract new
signCount
Flow:
Client Server Database
│ │ │
│ POST /register/begin │ │
│────────────────────────>│ call webauthn_begin_registration(user_id)
│ │─────────────────────────────>│
│ │<─────────────────────────────│ {challenge, user_handle, ...}
│ │ generateRegistrationOptions()│
│<────────────────────────│ {publicKey options} │
│ │ │
│ navigator.credentials │ │
│ .create(options) │ │
│ │ │
│ POST /register/finish │ │
│────────────────────────>│ verifyRegistrationResponse() │
│ │ call webauthn_finish_registration(...)
│ │─────────────────────────────>│
│ │<─────────────────────────────│ credential_id
│<────────────────────────│ {success: true} │
3. Frontend SDK
Wrapper around navigator.credentials.create/get + API calls:
auth.passkey.register()
auth.passkey.signIn()
auth.passkey.list()
auth.passkey.delete(id)
auth.passkey.rename(id, name)
Implementation Plan
Phase 1: HTTP Routes (~1 day)
Phase 2: Frontend SDK (~1 day)
Phase 3: Dashboard UI (optional, ~1 day)
Comparison with Supabase
| Feature |
Supabase Beta |
Constructive |
| Credential storage |
✅ |
✅ |
| Multi-device sync tracking |
? |
✅ |
| Backup state tracking |
? |
✅ |
| Clone detection (signCount) |
? |
✅ |
| Transport hints |
? |
✅ |
| Origin allowlist |
≤5 |
unlimited |
| Attestation config |
? |
✅ |
| User verification toggle |
? |
✅ |
| Resident key config |
? |
✅ |
| Challenge expiry config |
? |
✅ (300s default) |
| HTTP API |
✅ |
❌ needed |
| Frontend SDK |
✅ |
❌ needed |
File Locations
Database:
- Schema:
constructive-db/services/constructive-services/deploy/migrate/webauthn_*.sql
- Procedures:
constructive-db/packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/webauthn_auth/
- ORM types:
constructive-db/sdk/constructive-sdk/src/orm/input-types.ts
- ORM models:
constructive-db/sdk/constructive-sdk/src/orm/models/webauthn*.ts
Server:
- API context:
constructive/graphql/server/src/middleware/api.ts (已有 webauthnSettings)
- New routes:
constructive/graphql/server/src/middleware/webauthn.ts (待建)
References
Background
Supabase announced passkey authentication on 2026-05-28 (beta). After researching their implementation and auditing our codebase, Constructive already has a more complete WebAuthn foundation — we just need to wire up the HTTP layer.
Current State
✅ Database Layer (Ready)
Schema (
constructive-db):webauthn_credentialstable with full spec support:credential_id(Base64url, globally unique)public_key(COSE-encoded)sign_count(clone detection)webauthn_user_id(privacy-preserving handle)transports[](usb, nfc, ble, internal, hybrid)credential_device_type(singleDevice/multiDevice)backup_eligible,backup_state(sync passkey tracking)name(user label)last_used_atConfiguration (
webauthn_auth_module):rp_id,rp_nameorigin_allowlist[](unlimited, vs Supabase's 5-origin limit)attestation_typerequire_user_verificationresident_keychallenge_expiry✅ Stored Procedures (Ready)
Located in
constructive-db/packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/webauthn_auth/:webauthn_begin_registration(p_user_id uuid) → jsonbwebauthn_finish_registration(p_credential_id text, p_public_key bytea, p_sign_count bigint, p_transports text[], p_credential_device_type text, p_backup_eligible boolean, p_backup_state boolean, p_webauthn_user_id text, p_user_id uuid, p_name text) → uuidwebauthn_begin_sign_in(p_user_id uuid DEFAULT NULL) → jsonbwebauthn_finish_sign_in(p_credential_id text, p_new_sign_count bigint, p_new_backup_state boolean, credential_kind text) → (user_id uuid, access_token text, access_token_expires_at timestamptz)Key implementation details:
session_secretstable, scoped tojwt_private.current_session_id()DELETE ... RETURNINGSECURITY DEFINERin private schema (not exposed to authenticated role)finish_sign_inreturns same signature assign_in_identity/sign_in_magic_linkfor consistent downstream handling✅ ORM Models (Ready)
WebauthnCredentialModel- full CRUDWebauthnAuthModuleModel- config managementWebauthnSettingModel- per-schema settingswebauthnSettingsloaded into API context ✅What's Missing
1. HTTP API Endpoints
2. WebAuthn Verification Layer
Need
@simplewebauthn/serverto:credentialId,publicKey,signCount, etc.signCountFlow:
3. Frontend SDK
Wrapper around
navigator.credentials.create/get+ API calls:Implementation Plan
Phase 1: HTTP Routes (~1 day)
@simplewebauthn/serverdependency tographql/servermiddleware/webauthn.tswith 4 endpointswebauthnSettingscontextPhase 2: Frontend SDK (~1 day)
@simplewebauthn/browserdependencyconstructive-sdkPhase 3: Dashboard UI (optional, ~1 day)
Comparison with Supabase
File Locations
Database:
constructive-db/services/constructive-services/deploy/migrate/webauthn_*.sqlconstructive-db/packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/webauthn_auth/constructive-db/sdk/constructive-sdk/src/orm/input-types.tsconstructive-db/sdk/constructive-sdk/src/orm/models/webauthn*.tsServer:
constructive/graphql/server/src/middleware/api.ts(已有webauthnSettings)constructive/graphql/server/src/middleware/webauthn.ts(待建)References