Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ The `membership_types` array is a top-level key in the blueprint definition JSON
"has_limits": false,
"has_profiles": false,
"has_levels": false,
"skip_entity_policies": false
"skip_entity_policies": false,
"table_provision": null
}
],
"tables": [ ... ],
Expand All @@ -34,11 +35,114 @@ The `membership_types` array is a top-level key in the blueprint definition JSON
| `description` | string | No | `null` | Optional description of the entity type |
| `parent_entity` | string | No | `"org"` | Parent type prefix. Must be an already-provisioned type |
| `table_name` | string | No | `prefix + 's'` | Override entity table name (e.g. `"rooms"` instead of default `"channels"`) |
| `is_visible` | boolean | No | `true` | Whether parent members can see child entities |
| `is_visible` | boolean | No | `true` | Gates the default `parent_member` SELECT policy. **No-op when `table_provision` is supplied.** See [Entity-Table Policies](#entity-table-policies-is_visible-skip_entity_policies-table_provision) |
| `has_limits` | boolean | No | `false` | Provision a `limits_module` for this type |
| `has_profiles` | boolean | No | `false` | Provision a `profiles_module` for named permission roles |
| `has_levels` | boolean | No | `false` | Provision a `levels_module` for gamification |
| `skip_entity_policies` | boolean | No | `false` | Skip creating default RLS policies on the entity table |
| `skip_entity_policies` | boolean | No | `false` | Escape hatch: apply zero policies on the entity table. See [Entity-Table Policies](#entity-table-policies-is_visible-skip_entity_policies-table_provision) |
| `table_provision` | object | No | `null` | Override object for the entity table (nodes, fields, grants, policies). When supplied, `policies[]` **replaces** the five default entity-table policies. See [Entity-Table Policies](#entity-table-policies-is_visible-skip_entity_policies-table_provision) |

## Entity-Table Policies (`is_visible`, `skip_entity_policies`, `table_provision`)

The entity table itself (e.g. `channels`) needs RLS policies so members can see / create / update / delete their own entities. Three fields interact to decide what ends up on that table:

### Decision matrix

| `skip_entity_policies` | `table_provision` | Result on the entity table |
|---|---|---|
| `false` (default) | `null` (default) | **5 defaults** (gated by `is_visible`) |
| `false` | object | **caller's `policies[]` only**; `is_visible` is a no-op |
| `true` | `null` | **0 policies** (escape hatch — you add them later) |
| `true` | object | **caller's `policies[]` only** |

**Mental model:** "defaults **OR** your overlay, never both." The presence of `table_provision` = "I know what I'm doing, give me full control." `is_visible` only matters on the defaults path.

### The 5 default policies

When `table_provision` is `null` and `skip_entity_policies` is `false`, the following policies are applied to the entity table (via `secure_table_provision` fanout):

| Default | Privilege | Summary |
|---|---|---|
| `self_member` | `SELECT` | Members of this entity can see it |
| `parent_member` | `SELECT` | Members of the **parent** entity can see it **— only when `is_visible: true`** |
| `admin_create` | `INSERT` | Parent members with `create_entity` permission can create one |
| `admin_update` | `UPDATE` | Entity admins can update |
| `admin_delete` | `DELETE` | Entity admins can delete |

If `is_visible: false`, the `parent_member` SELECT default is omitted and sibling entities become invisible to parent members (other 4 defaults still apply).

### `table_provision` shape

`table_provision` mirrors the same vocabulary as `tables[]` entries / `secure_table_provision` — so if you already know blueprint tables, you already know this:

```json
{
"use_rls": true,
"nodes": [ { "$type": "DataTimestamps" } ],
"fields": [
{ "name": "topic", "type": "text" }
],
"grant_privileges": [ ["select", "*"], ["insert", "*"] ],
"grant_roles": ["authenticated"],
"policies": [
{
"$type": "AuthzEntityMembership",
"data": { "entity_field": "id", "entity_type": "channel" },
"privileges": ["select"],
"name": "self_member"
}
]
}
```

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `use_rls` | boolean | No | `true` | Whether to enable RLS on the entity table |
| `nodes` | array | No | `[]` | Data behavior nodes applied to the entity table (e.g. `DataTimestamps`) |
| `fields` | array | No | `[]` | Extra columns on the entity table |
| `grant_privileges` | array | No | inherited | Privilege tuples like `[["select","*"], ["insert","*"]]` |
| `grant_roles` | string[] | No | `["authenticated"]` | Roles that receive the grants |
| `policies` | array | No | `[]` | Safegres policy definitions (same `$type` discriminator as `tables[].policies[]`). When present, **fully replaces** the 5 defaults |

### When to use which

| Goal | Config |
|---|---|
| "Just give me the standard defaults" | leave all three fields at defaults |
| "Hide this entity from parent members but keep everything else" | `"is_visible": false` |
| "Add custom fields/grants on the entity table (no custom policies)" | `table_provision: { nodes, fields, grant_privileges }`. **Heads up:** because `table_provision` is the override flag, this skips the 5 default policies too. If you want custom nodes/fields **and** the default policies, also copy the 5 defaults into `table_provision.policies[]` |
| "I want a completely different policy model on this entity" | `table_provision: { policies: [...] }` with your own `policies[]` |
| "I'll add policies later myself" | `"skip_entity_policies": true` |

### Example: custom fields + custom policies

```json
{
"name": "Data Room Member",
"prefix": "data_room",
"parent_entity": "org",
"table_provision": {
"nodes": [ { "$type": "DataTimestamps" } ],
"fields": [
{ "name": "topic", "type": "text" }
],
"policies": [
{
"$type": "AuthzEntityMembership",
"data": { "entity_field": "id", "entity_type": "data_room" },
"privileges": ["select", "update", "delete"],
"name": "self_member"
},
{
"$type": "AuthzEntityMembership",
"data": { "entity_field": "owner_id", "entity_type": "org" },
"privileges": ["insert"],
"name": "org_insert"
}
]
}
}
```

## TypeScript Type

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,97 @@ const result = await db.entityTypeProvision.create({
| `description` | string | No | `null` | Description of the entity type |
| `parentEntity` | string | No | `"org"` | Parent type prefix |
| `tableName` | string | No | `prefix + 's'` | Override entity table name |
| `isVisible` | boolean | No | `true` | Parent members can see children |
| `isVisible` | boolean | No | `true` | Gates the default `parent_member` SELECT policy. **No-op when `tableProvision` is supplied.** See [Entity-Table Policies](#entity-table-policies-isvisible-skipentitypolicies-tableprovision) |
| `hasLimits` | boolean | No | `false` | Provision limits module |
| `hasProfiles` | boolean | No | `false` | Provision profiles module |
| `hasLevels` | boolean | No | `false` | Provision levels module |
| `skipEntityPolicies` | boolean | No | `false` | Skip default RLS policies |
| `skipEntityPolicies` | boolean | No | `false` | Escape hatch: apply zero default policies. See [Entity-Table Policies](#entity-table-policies-isvisible-skipentitypolicies-tableprovision) |
| `tableProvision` | object | No | `null` | Override for the entity table (nodes, fields, grants, policies). When supplied, `policies[]` **replaces** the 5 default entity-table policies. See [Entity-Table Policies](#entity-table-policies-isvisible-skipentitypolicies-tableprovision) |

## Entity-Table Policies (`isVisible`, `skipEntityPolicies`, `tableProvision`)

The entity table (e.g. `channels`) needs RLS policies so members can see / create / update / delete their own entities. Three fields decide what lands on the table:

### Decision matrix

| `skipEntityPolicies` | `tableProvision` | Result on the entity table |
|---|---|---|
| `false` (default) | `null` (default) | **5 defaults** (gated by `isVisible`) |
| `false` | object | **caller's `policies[]` only**; `isVisible` is a no-op |
| `true` | `null` | **0 policies** (escape hatch — you add them later) |
| `true` | object | **caller's `policies[]` only** |

**Mental model:** "defaults **OR** your overlay, never both." The presence of `tableProvision` means "I know what I'm doing, give me full control." `isVisible` only matters on the defaults path.

### The 5 default policies

When `tableProvision` is `null` and `skipEntityPolicies` is `false`:

| Default | Privilege | Summary |
|---|---|---|
| `self_member` | `SELECT` | Members of this entity can see it |
| `parent_member` | `SELECT` | Members of the **parent** entity can see it **— only when `isVisible: true`** |
| `admin_create` | `INSERT` | Parent members with `create_entity` permission can create one |
| `admin_update` | `UPDATE` | Entity admins can update |
| `admin_delete` | `DELETE` | Entity admins can delete |

### `tableProvision` shape

Same vocabulary as blueprint `tables[]` entries / `secure_table_provision`:

```typescript
const result = await db.entityTypeProvision.create({
data: {
databaseId: dbId,
name: 'Data Room Member',
prefix: 'data_room',
parentEntity: 'org',
tableProvision: {
use_rls: true,
nodes: [{ $type: 'DataTimestamps' }],
fields: [{ name: 'topic', type: 'text' }],
grant_privileges: [['select', '*'], ['insert', '*']],
grant_roles: ['authenticated'],
policies: [
{
$type: 'AuthzEntityMembership',
data: { entity_field: 'id', entity_type: 'data_room' },
privileges: ['select', 'update', 'delete'],
name: 'self_member',
},
{
$type: 'AuthzEntityMembership',
data: { entity_field: 'owner_id', entity_type: 'org' },
privileges: ['insert'],
name: 'org_insert',
},
],
},
},
select: { outMembershipType: true, outEntityTableId: true },
}).execute();
```

| Field | Type | Default | Description |
|---|---|---|---|
| `use_rls` | boolean | `true` | Enable RLS on the entity table |
| `nodes` | array | `[]` | Data behavior nodes applied to the entity table (e.g. `DataTimestamps`) |
| `fields` | array | `[]` | Extra columns on the entity table |
| `grant_privileges` | array | inherited | Privilege tuples (e.g. `[["select","*"], ["insert","*"]]`) |
| `grant_roles` | string[] | `["authenticated"]` | Roles that receive the grants |
| `policies` | array | `[]` | Safegres policy definitions. When present, **fully replaces** the 5 defaults |

> **snake_case inside the object:** `tableProvision` is a JSONB payload, so its inner keys use snake_case (`grant_privileges`, `grant_roles`, `use_rls`) — the same convention as blueprint `tables[]` entries. The outer `tableProvision` key itself is camelCase because it's an ORM column name.

### When to use which

| Goal | Config |
|---|---|
| "Standard defaults" | leave all three fields at defaults |
| "Hide from parent members" | `isVisible: false` |
| "Custom fields/grants on the entity table (no custom policies)" | `tableProvision: { nodes, fields, grant_privileges }`. **Heads up:** because `tableProvision` is the override flag, this skips the 5 default policies too. If you want custom fields **and** defaults, also copy the 5 defaults into `tableProvision.policies[]` |
| "Completely different policy model" | `tableProvision: { policies: [...] }` |
| "I'll add policies later" | `skipEntityPolicies: true` |

### Output fields

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ The blueprint `definition` is a JSONB document that declaratively describes a co
| `description` | string | No | `null` | Description of the entity type |
| `parent_entity` | string | No | `"org"` | Parent type prefix. Must be already provisioned |
| `table_name` | string | No | `prefix + 's'` | Override entity table name |
| `is_visible` | boolean | No | `true` | Whether parent members can see children |
| `is_visible` | boolean | No | `true` | Gates the default `parent_member` SELECT policy. No-op when `table_provision` is supplied |
| `has_limits` | boolean | No | `false` | Provision a limits module |
| `has_profiles` | boolean | No | `false` | Provision a profiles module (named permission roles) |
| `has_levels` | boolean | No | `false` | Provision a levels module (gamification) |
| `skip_entity_policies` | boolean | No | `false` | Skip creating default RLS policies |
| `skip_entity_policies` | boolean | No | `false` | Escape hatch: apply zero default RLS policies on the entity table |
| `table_provision` | object | No | `null` | Override object for the entity table (shape mirrors `tables[]`: `nodes`, `fields`, `grant_privileges`, `grant_roles`, `use_rls`, `policies`). When supplied, `policies[]` **replaces** the 5 default entity-table policies; `is_visible` becomes a no-op |

**Processing order:** Entries are processed in array order. Parent types must appear before child types.

Expand Down