Content adapter
The layer registers a PlutoContentAdapter backed by Supabase and PostgREST. It plugs into the generic content routes of core, /api/_pluto/content/[type]. A content type that a layer declares maps onto a table, with no server code for each type. See Content model.
The Nitro plugin registers the adapter at startup with registerContentAdapter.
What the adapter does not do
- No schema generation. A layer writes its SQL migration by hand. The adapter only maps a content type onto a table that already exists.
- No relation resolution. A
referencefield is stored and read back as a plain column value. Its options come fromoptionsUrl. - No
describe(). The optional conformance warning is not part of this adapter.
What the adapter does
The adapter has the id supabase. It implements list, get, create, update, remove, authorize, and countCreated.
listcounts the rows. It filters on the status column unless the caller asks for unpublished items.searchis a case-insensitive match on the title column.getlooks up the primary key first. If no row matches, and the type has aslug, it looks up the slug column.authorizecallsrequireCapabilityfor the capability that the generic route asks for.
Field and column mapping
The adapter uses fieldColumn, mapFieldsToColumns, and mapColumnsToFields of core for every translation between a field payload and a database row. For titleField, defaultSort.field, and slug.field, it finds the field first and then calls fieldColumn. If the field does not exist, it guesses the column from the name and logs a warning.
Timestamps
When a timestamp column resolves, create and update always overwrite it with the current time. A client never controls its own creation or update time. A timestamp column is off when timestamps is false, or when the key is false.
Status
- A new row gets the default status only when the payload does not set the status column. An explicit status is respected.
updatesetspublishedAtColumnto the current time the first time a row gets thepublishedValue. It never overwrites a value that is already set on a later edit.- A status other than
publishedValueleavespublishedAtColumnalone. The adapter never clears it.
Errors
The adapter logs the raw error first. Then it maps the Postgres error.code to an error that is safe to send:
| Postgres code | Meaning | HTTP status | Message |
|---|---|---|---|
23505 | Unique violation | 409 | A record with this value already exists. |
23503 | Foreign key violation | 400 | This references a record that does not exist. |
| Any other | 500 | The raw error.message |
Counting by date
The adapter implements countCreated with one exact-count HEAD query for each range. The dashboard activity chart uses it.
Types are loose here, on purpose
The table name of a content type is a runtime string. The generated Database type cannot know a table that another repository declares. So this one file uses an untyped SupabaseClient. This is deliberate. Every other file in the layer keeps the typed client.
Use it from a layer
A feature layer declares the content type and registers it. The adapter handles the storage. supabase-shop and supabase-blog work this way. See Shop (Supabase) and Blog (Supabase).