title: Hooks description: Run custom logic at key points in the server lifecycle.#
Hooks let you run custom logic at key points in the hatk lifecycle, like when a user logs in via OAuth. Define them with defineHook() in the hooks/ directory.
on-login#
The on-login hook runs after a successful OAuth login. The most common use is calling ensureRepo to backfill the user's data so it's available immediately:
// hooks/on-login.ts
import { defineHook } from '$hatk'
export default defineHook('on-login', async (ctx) => {
await ctx.ensureRepo(ctx.did)
})
This is three lines, but it's important: without it, a new user's existing records won't appear until the firehose (the AT Protocol's real-time event stream) delivers them. ensureRepo fetches the user's repository from their PDS, indexes it, and waits for the backfill to complete before returning.
Populating records on first login#
Since the hook has full database and record access, you can check for records and create them if needed. For example, copying a user's Bluesky profile to a custom profile collection on first login:
// hooks/on-login.ts
import { defineHook, type BskyActorProfile, type MyAppProfile } from '$hatk'
export default defineHook('on-login', async (ctx) => {
await ctx.ensureRepo(ctx.did)
// Check if user already has an app profile
const existing = await ctx.lookup<MyAppProfile>('my.app.profile', 'did', [ctx.did])
if (existing.has(ctx.did)) return
// Copy from Bluesky profile
const bsky = await ctx.lookup<BskyActorProfile>('app.bsky.actor.profile', 'did', [ctx.did])
const profile = bsky.get(ctx.did)
if (!profile) return
await ctx.createRecord('my.app.profile', {
displayName: profile.value.displayName,
description: profile.value.description,
avatar: profile.value.avatar,
createdAt: new Date().toISOString(),
}, { rkey: 'self' })
})
Hook context#
The on-login handler receives a context object with database access, record helpers, and the login event data:
| Field | Type | Description |
|---|---|---|
did |
string |
The DID of the user who logged in |
ensureRepo |
(did: string) => Promise<void> |
Backfills the user's repo from their PDS and waits for completion |
db.query |
(sql, params?) => Promise<unknown[]> |
Run a read query |
db.run |
(sql, params?) => Promise<void> |
Run a write query (INSERT, UPDATE, DELETE) |
lookup |
(collection, field, values) => Promise<Map> |
Look up records by field values |
count |
(collection, field, values) => Promise<Map> |
Count records by field values |
getRecords |
(collection, uris) => Promise<Map> |
Fetch records by URI |
labels |
(uris) => Promise<Map> |
Get labels for URIs |
blobUrl |
(did, ref, preset?) => string |
Generate a blob URL |
createRecord |
(collection, record, opts?) => Promise<{uri?, cid?}> |
Write a record to the user's PDS and index locally |
putRecord |
(collection, rkey, record) => Promise<{uri?, cid?}> |
Create or update a record on the user's PDS |
deleteRecord |
(collection, rkey) => Promise<void> |
Delete a record from the user's PDS and local index |
Error handling#
If a hook throws, the error is logged but does not block the login flow. The user still completes authentication successfully. Hooks have a 30-second timeout — if the hook takes longer, it is cancelled and the login proceeds.