Identities
WIPThe account row, typing your own profile, provider links, email verification, soft delete and erasure, bulk import, and GDPR export
An identity is an account: one row with your profile fields, the provider logins linked to it, and whether its address was proven. auth.identities reads and writes it.
The row
| Field | Meaning |
|---|---|
id | Generated by the store. |
profile | Your fields. email and username are required and unique; everything else is yours. |
providers | Linked logins, each { providerId, providerSub, addedAt, addedBy }, such as oauth:google and Google's sub. |
emailVerified | Whether the address was proven by a link or a verified IdP, not merely typed. |
version | Optimistic-concurrency counter; updateProfile takes it and refuses a stale write. |
deletedAt | Set by a soft delete; the row reads as absent while it is set. |
createdBy, updatedBy, deletedBy | The actor each write was attributed to, or null. |
Typing the profile
The profile type flows from the engine's first type parameter:
import { MemoryAdapter } from '@gentleduck/auth/adapters/memory'
import { createAuth } from '@gentleduck/auth/core'
type Profile = { email: string; username: string; name: string; locale?: string }
const auth = createAuth<Profile>({
baseUrl: 'http://localhost:3000',
stores: new MemoryAdapter<Profile>(),
})
const ada = await auth.identities.create({ profile: { email: 'ada@example.com', username: 'ada', name: 'Ada' } })
ada.profile.name // string
The serialized profile is capped at 16 KiB by default; set identities: { profileMaxBytes } to change it, or 0 to remove the cap.
Reading
await auth.identities.getById(id)
await auth.identities.getByEmail('ada@example.com').orNull()
await auth.identities.getByProviderSub('oauth:github', '583231').orNull()
Each answers an Answer: awaiting it throws AUTH_IDENTITY_NOT_FOUND for a missing or soft-deleted row, and .orNull() answers null instead. Email lookups are case-insensitive.
Writing
const ada = await auth.identities.create({
profile: { email: 'ada@example.com', username: 'ada', name: 'Ada' },
emailVerified: false,
})
await auth.identities.updateProfile(ada.id, { name: 'Ada Lovelace' }, ada.version)
createemitssignup.completed, and refuses a taken email or username withAUTH_EMAIL_TAKENorAUTH_USERNAME_TAKEN.- Every write bumps
version, a link included, so read the row again before anupdateProfilethat follows one. updateProfilemerges the patch. A patch that changesemailclearsemailVerifiedin the same write, since a new address is unproven.markEmailVerified(id, email)sets the flag, and refuses once the identity no longer holds that address.flows.completeEmailVerificationcalls it for you.
Provider links
await auth.identities.link(ada.id, { providerId: 'oauth:github', providerSub: '583231' })
await auth.identities.unlink(ada.id, 'oauth:github')
link refuses a provider the identity already holds, and a login another identity holds (AUTH_PROVIDER_TAKEN). unlink refuses to drop the last way in: the remaining links plus credentials such as a password must not reach zero. flows.linkProvider and flows.unlinkProvider wrap both for a signed-in user.
Deleting
| Call | Effect |
|---|---|
softDelete(id) | Hides the row for the grace period (7 days by default, identities.softDeleteGracePeriodMs). |
restore(id) | Clears a soft delete; after the window it throws AUTH_GRACE_EXPIRED. |
erase(id, { reason, operatorId }) | Removes the row and everything under it for good, and emits identity.erased. |
gc() | Erases every row whose grace window has closed. Schedule it yourself. |
A soft delete does not end sessions; flows.completeAccountDeletion does both, and mails an undo link if asked.
Bulk and export
const result = await auth.identities.bulkCreate(rows, { mode: 'skipExisting' })
// { created, skipped, failed }
const blob = await auth.identities.exportAll(ada.id, auth.cfg.stores.credentials, {}, { sessions: auth.cfg.stores.sessions })
// { identity, credentials, sessions, schemaVersion: '1', exportedAt }
bulkCreate matches existing rows by email: skipExisting passes over them, merge links the new logins onto them, replace erases and recreates them. exportAll is the GDPR right-of-access export, with every credential secret and session CSRF hash removed.
softDeleteMany, restoreMany, eraseMany, updateProfileMany, linkMany and unlinkMany do the same over a set.