--- 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: ```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('my.app.profile', 'did', [ctx.did]) if (existing.has(ctx.did)) return // Copy from Bluesky profile const bsky = await ctx.lookup('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` | Backfills the user's repo from their PDS and waits for completion | | `db.query` | `(sql, params?) => Promise` | Run a read query | | `db.run` | `(sql, params?) => Promise` | Run a write query (INSERT, UPDATE, DELETE) | | `lookup` | `(collection, field, values) => Promise` | Look up records by field values | | `count` | `(collection, field, values) => Promise` | Count records by field values | | `getRecords` | `(collection, uris) => Promise` | Fetch records by URI | | `labels` | `(uris) => Promise` | 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` | 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.