.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
.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
corsair_entities:
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 referencecorsair_entities.id:
Checklist
A dashboard is wired correctly when:- Page loads and lists read from
.dbonly, with no.apicall 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.