~ knotless Search/
↑↓ move↵ open⇧↵ all results

peterullrich.com/hatk

AT Protocol app framework

Forked from chadtmiller.com/hatk

0
0
main
77 lines · 4.3 KB Raw History
---
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<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.