---
title: "Apply declarative authorization manifest"
method: POST
path: "/projects/v1/admin/{project_id}/expose"
tags: ["Admin"]
---

# Apply declarative authorization manifest

`POST /projects/v1/admin/{project_id}/expose`

## Request

- Base URL: `https://api.run402.com`
- URL: `https://api.run402.com/projects/v1/admin/{project_id}/expose`
- Auth: HTTP bearer

Apply a declarative manifest describing the project's full authorization surface (tables, views, RPCs). Idempotent: applying the same manifest twice is a no-op. Applying a new manifest converges state — policies, grants, triggers, and views no longer in the manifest are dropped. Lifecycle-gated for non-active projects. The same manifest can also be shipped inline as `database.expose` on a `POST /apply/v1/plans` request.

## Path parameters

- `project_id` string, required: Run402 project ID. Public IDs are not UUIDs.

## Request body

- object
  - `version` '1', required
  - `tables` object[]
    - `name` string, required
    - `expose` boolean, required: true grants anon SELECT and applies the chosen policy; false leaves the table dark (default).
    - `policy` 'user_owns_rows' | 'public_read_authenticated_write' | 'public_read_append_only' | 'public_read_write_UNRESTRICTED' | 'custom': Required when expose: true.
    - `owner_column` string: Required when policy = 'user_owns_rows'.
    - `force_owner_on_insert` boolean: Install a BEFORE INSERT trigger that sets owner_column := auth.uid() when NULL. Requires policy = 'user_owns_rows'.
    - `live` boolean: tenant-live-changes: emit change hints for this table over /live/v1 and /_run402/live (statement-level NOTIFY triggers installed by the converge). Requires expose: true and a built-in policy; refused on policy = 'custom'.
    - `i_understand_this_is_unrestricted` boolean: Required when policy = 'public_read_write_UNRESTRICTED'.
    - `custom_sql` string: Escape hatch — user-supplied SQL run after RLS is enabled+forced. Required when policy = 'custom'. Subject to SQL safety blocklist (no GRANT/REVOKE/ALTER SYSTEM).
  - `views` object[]
    - `name` string, required
    - `base` string, required: Underlying table.
    - `select` string[], required: Columns to project, or ["*"] for all.
    - `filter` string: Optional WHERE clause.
    - `expose` boolean: Grant SELECT to anon when true.
    - `security_invoker` boolean: Always coerced to true on apply — views inherit RLS from the base table.
  - `rpcs` object[]
    - `name` string, required
    - `signature` string, required: Parenthesized arg list, e.g. "(user_id uuid)" or "()".
    - `grant_to` string[], required: Roles to GRANT EXECUTE to (e.g. ['authenticated']).

## Response `200`

Manifest applied

- object
  - `status` string
  - `project_id` string: Run402 project ID. Public IDs are not UUIDs.
  - `applied` object
    - `tables` string[]
    - `views` string[]
    - `rpcs` string[]
  - `dropped` object
    - `tables` string[]
    - `views` string[]
    - `rpcs` string[]

## Other responses

- `400`: Invalid manifest (validation error names the offending path, e.g. `manifest.tables[2].owner_column`)
- `401`: Missing or invalid authentication. Run402-originated JSON bodies use the canonical ErrorEnvelope.
- `403`: Project is past_due/frozen/dormant — control-plane gated

---

[API](https://skmtc.dev/run402/apis/run402-api.md) · [All operations](https://skmtc.dev/run402/apis/run402-api/llms.txt) · [OpenAPI document](https://skmtc.dev/run402/apis/run402-api/revisions/893607557f50?raw)
