Skip to main content
A dashboard reads from .db and writes through .api. Page loads never touch the third-party service, so they’re fast and can’t be rate limited. A button that creates an issue or sends a message goes straight to the live API. This is the most common shape, and the one to reach for whenever the ask sounds like “show me my…”. Pattern: read .db to render, .api to mutate or refresh, then read .db again.

The read path

Query the plugin’s entity types directly. Nothing here makes a network call:
src/server/feed.ts
Entity data lives on .data in camelCase, typed from the plugin’s schema:
search accepts entity_id, a data object for top-level JSON fields, plus limit and offset. All filters combine with AND. String fields support contains, startsWith, endsWith, and in; numbers and dates support ranges. Each plugin’s filterable fields are listed in its database reference, such as GitHub.
search has no sort option. Order results in your own code after reading, or query corsair_entities directly with your ORM if you need SQL-level sorting and pagination.

The write path

Mutations and refreshes use .api. Corsair upserts the response into your database as part of the call, so the next .db read already reflects it:
src/server/actions.ts

Refresh buttons

A refresh is just a list call against .api. Because every response is upserted, you don’t wire up any sync logic. You call the endpoint and re-read:
src/server/refresh.ts
To show “last synced 4 minutes ago”, keep that timestamp in one of your own tables. It’s app state, not integration data, so it doesn’t belong in corsair_entities:
Prefer webhooks over a refresh button where the plugin supports them. Rows update the moment something changes upstream and the page is current with no user action. See Webhooks.

Adding your own columns

Integration data is rarely enough on its own. To attach private notes, tags, or review state, put them in your own table and reference corsair_entities.id:
Your notes stay yours; the integration side of the join keeps updating itself. Full example in Database.

Checklist

A dashboard is wired correctly when:
  • Page loads and lists read from .db only, with no .api call on render.
  • Creates, updates, and deletes go through .api.
  • Refresh calls .api, then re-reads .db, rather than merging responses into UI state by hand.
  • App-specific fields live in your tables, keyed to corsair_entities.id.
  • Every read and write is scoped with withTenant().

What’s next

Vibe code a dashboard

Scaffold a T3 app and have an agent build the whole UI from one prompt.

Add a chat command bar

Let people type “close #458 as duplicate” instead of clicking.

Keep it live with webhooks

Update rows on upstream changes instead of polling.

React client

Connection status and connect links as hooks.