Skip to content

effaced-supabase

effaced-supabase — first-party Supabase resolvers for effaced.

class PostgrestColumn(BaseModel):
category: PiiCategory
name: str = Field(min_length=1)

One PII-bearing column the PostgREST resolver exports and erases.

A column declares the name PostgREST exposes it under and the PiiCategory the value holds. Declaration is explicit and auditable on purpose: the resolver never discovers a table’s schema, so a column not named here is neither exported nor counted in the resolver’s covered surface.

Fields:

  • category (PiiCategory): The PiiCategory the column’s value is declared to hold; carried onto every exported record and into the resolver’s covered surface.
  • name (str): The column name as PostgREST exposes it (a select key).
class PostgrestTable(BaseModel):
columns: tuple[PostgrestColumn, ...] = Field(min_length=1)
name: str = Field(min_length=1)
subject_column: str = Field(min_length=1)

One PostgREST-exposed table holding a subject’s PII.

A table declares the column that carries the subject id and the PII-bearing columns to export and erase. The resolver filters on subject_column = <ref value> and never discovers the schema, so the declaration is the auditable record of which rows and columns the resolver reaches.

Fields:

  • columns (tuple[PostgrestColumn, ...]): The PII-bearing columns to export; at least one. The subject_column need not appear here unless it is itself PII to export.
  • name (str): The table (or view) name PostgREST exposes at /rest/v1/{name}.
  • subject_column (str): The column whose value equals the subject id — the resolver matches subject_column=eq.<ref value>.
class SupabaseAuthResolver:
def __init__(base_url: str, service_role_key: str, *, transport: httpx.BaseTransport | None = None, timeout: float = _DEFAULT_TIMEOUT) -> None

Exports and erases a subject’s Supabase Auth (auth.users) record.

Expects refs of kind "supabase_auth" (refs are routed to the resolver whose name equals their kind — ADR 0008) whose value is the GoTrue user id. Talks to the Admin API (/auth/v1/admin), which only accepts the service-role key — server-side use only.

Idempotency: a user GoTrue no longer knows yields already_absent=True — success, never an error.

Error taxonomy (see effaced_supabase.errors): 4xx responses other than 404 and 429 raise ResolverError; rate limits, 5xx, and connection faults propagate so the saga runner retries. A fresh httpx client is built per call — nothing loop- or connection-bound is cached on the instance (ADR 0006).

Fields:

  • covered_surface (CoveredSurface): The GoTrue PII this resolver claims to reach (AttestingResolver). Returns: AUTH_COVERED_SURFACE, built from the exporter’s field tuple so it cannot drift.
  • name (str): Stable resolver name recorded in manifests and audits.
async def erase_subject(ref: SubjectRef) -> ResolverErasure

Hard-delete the user in GoTrue (Art. 17).

Args:

  • ref (SubjectRef): kind="supabase_auth", value=<gotrue user id>.

Returns:

  • ResolverErasure — The outcome; already_absent=True if GoTrue already had no
  • ResolverErasure — such user.

Raises:

  • ResolverError — The key is invalid, lacks admin access, or the request was malformed — retrying cannot succeed.
async def export_subject(ref: SubjectRef) -> ResolverExport

Collect the user’s GoTrue-held contact fields (Art. 15).

Args:

  • ref (SubjectRef): kind="supabase_auth", value=<gotrue user id>.

Returns:

  • ResolverExport — The user’s email and phone when populated (the field
  • ResolverExport — set lives in effaced_supabase.auth_export_records);
  • ResolverExport — empty when GoTrue holds no such user.

Raises:

  • ResolverError — The key is invalid, lacks admin access, or the request was malformed — retrying cannot succeed.
class SupabasePostgrestResolver:
def __init__(base_url: str, service_role_key: str, tables: Sequence[PostgrestTable], *, transport: httpx.BaseTransport | None = None, timeout: float = _DEFAULT_TIMEOUT) -> None

Exports and erases a subject’s PII held in PostgREST-exposed tables.

Expects refs of kind "supabase_postgrest" (refs are routed to the resolver whose name equals their kind — ADR 0008) whose value is the subject id. The resolver is configured with an explicit list of PostgrestTable declarations — the tables and columns holding the subject’s PII and the column that carries the subject id. It performs no schema discovery: a table or column not declared is neither exported nor erased.

For each table, export issues GET /rest/v1/{table}?{subject_column}=eq.{id}&select=... and emits one record per populated declared column; erasure issues DELETE /rest/v1/{table}?{subject_column}=eq.{id} with Prefer: return=representation. PostgREST answers a no-match delete with an empty representation (not a 404); a subject whose every declared table deletes nothing yields already_absent=True — success, never an error.

Talks to the data API with the service-role key, which bypasses row-level security — server-side use only. A fresh httpx client is built per call; nothing loop- or connection-bound is cached on the instance (ADR 0006). Error taxonomy is shared with the other Supabase resolvers (see effaced_supabase.errors): 4xx other than 429 raise ResolverError; rate limits, 5xx, and connection faults propagate so the saga runner retries.

Fields:

  • covered_surface (CoveredSurface): The declared PII this resolver reaches (AttestingResolver). Returns: A CoveredSurface with one field per declared column, built from the same table list the exporter walks so the declaration and the export cannot drift.
  • name (str): Stable resolver name recorded in manifests and audits.
async def erase_subject(ref: SubjectRef) -> ResolverErasure

Delete the subject’s rows from the declared tables (Art. 17).

Args:

  • ref (SubjectRef): kind="supabase_postgrest", value=<subject id>.

Returns:

  • ResolverErasure — The outcome; already_absent=True when no declared table
  • ResolverErasure — held a row for the subject.

Raises:

  • ResolverError — The key is invalid, lacks access, a declared table is missing, or the request was malformed — retrying cannot succeed.
async def export_subject(ref: SubjectRef) -> ResolverExport

Collect the subject’s PII across the declared tables (Art. 15).

Args:

  • ref (SubjectRef): kind="supabase_postgrest", value=<subject id>.

Returns:

  • ResolverExport — One record per populated declared column of every matching
  • ResolverExport — row, sourced under each table’s name; empty when no declared
  • ResolverExport — table holds the subject.

Raises:

  • ResolverError — The key is invalid, lacks access, a declared table is missing, or the request was malformed — retrying cannot succeed.