Skip to content

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:

typescript
// 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:

typescript
// 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:

FieldTypeDescription
didstringThe 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?) => stringGenerate 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.