Supabase

Content adapter

The Supabase adapter maps a content type to a table for the generic content routes.

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 reference field is stored and read back as a plain column value. Its options come from optionsUrl.
  • 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.

  • list counts the rows. It filters on the status column unless the caller asks for unpublished items. search is a case-insensitive match on the title column.
  • get looks up the primary key first. If no row matches, and the type has a slug, it looks up the slug column.
  • authorize calls requireCapability for 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.
  • update sets publishedAtColumn to the current time the first time a row gets the publishedValue. It never overwrites a value that is already set on a later edit.
  • A status other than publishedValue leaves publishedAtColumn alone. 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 codeMeaningHTTP statusMessage
23505Unique violation409A record with this value already exists.
23503Foreign key violation400This references a record that does not exist.
Any other500The 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).