# Record lists and My work

> Show record binding columns on a list page, keep its state in the URL, merge a record's bindings into one timeline, and list what waits on the caller across blocks.

Source: https://bettersupabase.com/docs/repository/record-lists

[Record bindings](/docs/repository/record-bindings) load the parts of one record page.
The same bindings also fill list columns, feed a record's timeline and,
through the subjects block, a "My work" page across every installed block.
All of it lives in `better-supabase/records`, with Server Component helpers in
`better-supabase/next` and a URL state hook in `better-supabase/react`.

## A list with binding columns [#a-list-with-binding-columns]

`defineRecordList` pairs a [list query](/docs/platform/list) with a record
type and names the bindings shown as columns. Each column is filled for
the whole page in one wave, never one query per row:

| Column  | Cell                                                                     |
| ------- | ------------------------------------------------------------------------ |
| `count` | how many rows the binding has, from its `count` (one query for the page) |
| `data`  | the binding's rows, from its `column`, or one `load` per row in one wave |

```ts title="src/features/customers/customer-record-list.ts"
import "server-only";
import { recordListServer } from "better-supabase/next";
import { defineRecordList } from "better-supabase/records";

import { customerList } from "@/lib/lists";
import { customerRecord } from "@/lib/supabase/records/customer";
import { bsb } from "@/lib/supabase/server";

export const customerRecordList = defineRecordList(
  customerRecord,
  customerList,
  { columns: { comments: "count" } },
);

export const customerRecords = recordListServer(bsb, customerRecordList);
```

`load(searchParams)` parses the URL state, runs the list query and fills the
columns. It returns `{ query, page, rows, filterFields, can }`: each row holds
its `subject`, its `row` and its `cells`, and every cell settles on its own
like a binding (`ok`, `error` or `hidden`). A column the caller can't read is
`hidden` on every row and runs no query. Bad URL state fails with a
`validation` error that names the parameter:

```ts title="src/app/api/customers/records/route.ts"
import { customerRecords } from "@/features/customers/customer-record-list";

export async function GET(request: Request): Promise<Response> {
  const result = await customerRecords.load(new URL(request.url).searchParams);

  if (!result.ok) {
    return Response.json(result.error, {
      status: result.error.kind === "validation" ? 400 : 500,
    });
  }

  return Response.json(result.data);
}
```

`recordListServer` returns plain data (Temporal values become ISO strings), so
the page passes straight to a Client Component. Outside Next.js, call
`customerRecordList.load({ db, clients, permissions }, searchParams)` with the
repositories, the record type's block clients and the caller's permission set.

## Keep the list state in the URL [#keep-the-list-state-in-the-url]

`useRecordListState` keeps a list's sort, facets, filter and page in the URL
through [nuqs](https://nuqs.dev). Pass it the list definition and the
`useQueryStates` result for the list's parsers:

```tsx title="src/features/customers/use-customer-list-state.ts"
"use client";

import { useRecordListState } from "better-supabase/react";
import { createParser, useQueryStates } from "nuqs";

import { customerList } from "@/lib/lists";

const parsers = customerList.nuqs(createParser);

export function useCustomerListState() {
  return useRecordListState(customerList, useQueryStates(parsers));
}
```

It returns `query` (the typed, parsed state, or the list's defaults when the
URL holds something the list rejects), `searchParams` for the fetch, `set` to
change keys (which goes back to page 1), `setPage` and `reset`. Wrap the app
in nuqs's `NuqsAdapter` once.

## A record's timeline [#a-records-timeline]

`loadTimeline(source, subject)` loads every binding that maps its rows to
timeline entries and merges them, newest first. The comments, versions and
attachments bindings do this; a block's own binding adds a `timeline` hook
that turns a row into `{ id, kind, at, actor?, row }`. A binding that fails or
that the caller can't read adds nothing and is listed in `skipped`:

```ts title="src/features/customers/customer-record-queries.ts"
const timeline = await customerPage.timeline(customerId, { limit: 20 });
timeline.entries; // [{ binding: "comments", kind: "comment", at, actor, row }]
timeline.skipped; // { versions: { kind: "hidden" } }
```

`bsb.record(...).timeline` checks the caller's permissions first, so it never
queries a binding they can't read. Pass `bindings` to merge only some of them.

## My work [#my-work]

The subjects block installs `my_work(tenant, page_size)`: what waits on the
caller across the installed blocks, newest first. It runs as the caller, so
every row is one they can read.

| Kind       | From        | Rows                                              |
| ---------- | ----------- | ------------------------------------------------- |
| `assigned` | `tasks`     | open work items assigned to the caller            |
| `approval` | `approvals` | pending approval requests the caller can decide   |
| `reminder` | `reminders` | the caller's reminders sent in the last 7 days    |
| `mention`  | `comments`  | comments that mention the caller, last 30 days    |
| `sla`      | `sla`       | SLA clocks that warned on the caller's work items |

A kind whose block isn't installed returns nothing. `loadMyWork` groups the
items by kind and counts them for tab badges, in one round trip:

```ts title="src/features/my-work/my-work-queries.ts"
import { loadMyWork } from "better-supabase/records";

const work = await loadMyWork(db.subjects, { tenant }).orThrow();

work.groups.approval; // approvals the caller can decide
work.counts.assigned; // 3
```

Each item has a `kind`, the `subject` it is about, the `ref` of the row that
put it there, and its `title`, `url` and `at` when the record has them.

The example app's customer list, customer timeline and `/my-work` page show
all four parts together.