
Constructive Data Modeling
- 2 installs
- Updated August 4, 2026
- constructive-io/constructive-skills
Tables, fields, relations, constraints, indexes, enums, and database provisioning via the type-safe SDK.
About
Constructive Data Modeling Tables, fields, relations, constraints, and indexes comprise the full schema lifecycle via the typesafe SDK.. Everything compiles to PostgreSQL DDL.
- Creating tables, fields, relations, constraints, or indexes via the SDK
- Provisioning databases with module selection
Constructive Data Modeling by the numbers
- 2 all-time installs (skills.sh)
- Ranked #741 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/constructive-io/constructive-skills --skill constructive-data-modelingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| Last updated | August 4, 2026 |
| Repository | constructive-io/constructive-skills ↗ |
What it does
Tables, fields, relations, constraints, indexes, enums, and database provisioning via the type-safe SDK.
Files
Constructive Data Modeling
Tables, fields, relations, constraints, and indexes — the full schema lifecycle via the type-safe SDK. Everything compiles to PostgreSQL DDL through Constructive's metaschema layer.
When to Apply
Use this skill when:
- Creating tables, fields, relations, constraints, or indexes via the SDK
- Provisioning databases with module selection
- Defining enum types
- Configuring field validation (regexp, min, max)
- Setting
api_requiredon nullable FK columns - Understanding the composition: table → fields → constraints → indexes → relations → security
The Composition Flow
1. Provision database → db.databaseProvisionModule.create({ modules: ['all'] })
2. Create table → db.secureTableProvision.create({ tableName, nodeType, ... })
3. Add fields → db.field.create({ tableId, name, type, ... })
4. Add constraints → db.checkConstraint.create / db.foreignKeyConstraint.create
5. Add indexes → db.index.create({ tableId, fieldIds, ... })
6. Add relations → db.relationProvision.create({ fromTableId, toTableId, ... })
7. Apply security → see constructive-security skillDatabase Provisioning
const result = await db.databaseProvisionModule.create({
data: {
databaseName: 'my-app',
ownerId: userId,
subdomain: 'my-app',
domain: 'localhost',
modules: ['all'],
bootstrapUser: true,
},
select: { id: true, databaseId: true, status: true },
}).execute();See provisioning.md for the full provisioning flow.
Tables
Create tables via secureTableProvision (recommended) or db.table.create:
await db.secureTableProvision.create({
data: {
databaseId,
tableName: 'projects',
nodeType: 'DataEntityMembership',
useRls: true,
grantRoles: ['authenticated'],
grantPrivileges: [['select', '*'], ['insert', '*'], ['update', '*'], ['delete', '*']] as unknown as Record<string, unknown>,
policyType: 'AuthzEntityMembership',
policyPermissive: true,
policyData: { entity_field: 'entity_id', membership_type: 2 },
},
select: { id: true, tableId: true, outFields: true },
}).execute();For full table management operations, see the generated orm-* skills in constructive-db.
Fields
await db.field.create({
data: {
databaseId,
tableId,
name: 'status',
type: { name: 'project_status' }, // enum type
defaultValue: { value: 'draft' },
isRequired: true,
},
select: { id: true },
}).execute();Field types include: text, integer, bigint, boolean, uuid, jsonb, timestamptz, date, numeric, citext, ltree, vector(N), and custom enums.
See field-types.md for the complete type reference.
Enum Types
await db.enum.create({
data: {
databaseId,
schemaName: 'app_public',
name: 'project_status',
values: ['draft', 'active', 'archived'],
},
select: { id: true, name: true, values: true },
}).execute();Relations
Four relation types via db.relationProvision.create:
| Type | Description |
|---|---|
BelongsTo | FK on source → target PK (default) |
HasMany | FK on target → source PK |
HasOne | FK on target → source PK (unique) |
ManyToMany | Junction table auto-created |
await db.relationProvision.create({
data: {
databaseId,
fromTableId: projectsTableId,
toTableId: organizationsTableId,
fromFieldName: 'organization_id',
apiRequired: true,
cascadeDelete: 'no_action',
},
select: { id: true },
}).execute();Constraints
Check constraints and foreign keys via db.checkConstraint.create and db.foreignKeyConstraint.create.
Indexes
await db.index.create({
data: {
databaseId,
tableId,
fieldIds: [fieldId],
isUnique: true,
accessMethod: 'btree', // or 'gin', 'gist', 'hash'
},
select: { id: true },
}).execute();api_required (Required API Fields)
For nullable FK columns that should be required at the GraphQL API level:
await db.field.update({
where: { id: fieldId },
data: { apiRequired: true },
select: { id: true },
}).execute();References
| File | Content |
|---|---|
| field-types.md | Complete field type reference |
| provisioning.md | Full database provisioning flow |
Cross-References
- Security (RLS, grants, policies): `constructive-security`
- Blueprint definitions: `constructive-blueprints`
- Generated ORM API: `constructive-orm`
- Code generation pipeline: `constructive-codegen`
FieldType & FieldDefault Reference
Structured JSONB representations of PostgreSQL types and default value expressions. Stored in metaschema_public.field.type and metaschema_public.field.default_value.
TypeScript interfaces: FieldType, FieldDefault from @constructive-io/node-type-registry.
---
FieldType
interface FieldType {
name: string; // SQL type name (required)
schema?: string; // schema qualifier
args?: (string | number | boolean)[]; // type arguments
array_dimensions?: number; // 1 = text[], 2 = text[][]
range?: string[]; // interval field range
}Examples
| FieldType | SQL |
|---|---|
{ name: 'text' } | text |
{ name: 'uuid' } | uuid |
{ name: 'boolean' } | boolean |
{ name: 'integer' } | integer |
{ name: 'bigint' } | bigint |
{ name: 'citext' } | citext |
{ name: 'jsonb' } | jsonb |
{ name: 'timestamptz' } | timestamptz |
{ name: 'date' } | date |
{ name: 'interval' } | interval |
{ name: 'numeric', args: [10, 2] } | numeric(10,2) |
{ name: 'varchar', args: [255] } | varchar(255) |
{ name: 'character', args: [1] } | character(1) |
{ name: 'bit', args: [8] } | bit(8) |
{ name: 'vector', args: [768] } | vector(768) |
{ name: 'vector', args: [1536] } | vector(1536) |
{ name: 'geometry', args: ['Point', 4326] } | geometry(Point,4326) |
{ name: 'geometry', args: ['Polygon', 4326] } | geometry(Polygon,4326) |
{ name: 'text', array_dimensions: 1 } | text[] |
{ name: 'citext', array_dimensions: 1 } | citext[] |
{ name: 'integer', array_dimensions: 1 } | integer[] |
{ name: 'integer', array_dimensions: 2 } | integer[][] |
{ name: 'jsonb', array_dimensions: 1 } | jsonb[] |
{ name: 'interval', range: ['day', 'second'] } | interval day to second |
{ name: 'interval', range: ['year', 'month'] } | interval year to month |
{ name: 'my_type', schema: 'my_schema' } | my_schema.my_type |
---
FieldDefault
type FieldDefaultArg = string | number | boolean | null | FieldDefault;
interface FieldDefault {
value?: string | number | boolean | null | unknown[] | Record<string, unknown>;
function?: string;
schema?: string;
args?: FieldDefaultArg[];
cast?: FieldType; // reuses FieldType shape
operator?: string;
left?: FieldDefault;
right?: FieldDefault;
sql_keyword?: string;
}Literal values
| FieldDefault | SQL |
|---|---|
{ value: 'draft' } | 'draft' |
{ value: '' } | '' |
{ value: true } | true |
{ value: false } | false |
{ value: 0 } | 0 |
{ value: 100 } | 100 |
Cast expressions
| FieldDefault | SQL |
|---|---|
{ value: {}, cast: { name: 'jsonb' } } | '{}'::jsonb |
{ value: [], cast: { name: 'jsonb' } } | '[]'::jsonb |
{ value: [], cast: { name: 'text', array_dimensions: 1 } } | '{}'::text[] |
{ value: '30 minutes', cast: { name: 'interval' } } | '30 minutes'::interval |
{ value: '15 minutes', cast: { name: 'interval' } } | '15 minutes'::interval |
Function calls
| FieldDefault | SQL |
|---|---|
{ function: 'now' } | now() |
{ function: 'uuidv7' } | uuidv7() |
{ function: 'gen_random_uuid' } | gen_random_uuid() |
{ function: 'current_user_id', schema: 'jwt_public' } | jwt_public.current_user_id() |
Function calls with arguments
| FieldDefault | SQL |
|---|---|
{ function: 'encode', args: [{ function: 'gen_random_bytes', args: [16] }, 'hex'] } | encode(gen_random_bytes(16), 'hex') |
{ function: 'lpad', args: ['', 32, '0'], cast: { name: 'bit', args: [32] } } | lpad('', 32, '0')::bit(32) |
Operator expressions
| FieldDefault | SQL |
|---|---|
{ operator: '+', left: { function: 'now' }, right: { value: '5 minutes', cast: { name: 'interval' } } } | now() + '5 minutes'::interval |
{ operator: '+', left: { function: 'now' }, right: { value: '30 minutes', cast: { name: 'interval' } } } | now() + '30 minutes'::interval |
SQL keywords
| FieldDefault | SQL |
|---|---|
{ sql_keyword: 'CURRENT_TIMESTAMP' } | CURRENT_TIMESTAMP |
{ sql_keyword: 'CURRENT_USER' } | CURRENT_USER |
---
Blueprint usage
{
"fields": [
{ "name": "title", "type": { "name": "text" }, "is_required": true },
{ "name": "status", "type": { "name": "text" }, "default": { "value": "draft" } },
{ "name": "metadata", "type": { "name": "jsonb" }, "default": { "value": {}, "cast": { "name": "jsonb" } } },
{ "name": "tags", "type": { "name": "citext", "array_dimensions": 1 }, "default": { "value": [], "cast": { "name": "citext", "array_dimensions": 1 } } },
{ "name": "expires_at", "type": { "name": "timestamptz" }, "default": { "operator": "+", "left": { "function": "now" }, "right": { "value": "5 minutes", "cast": { "name": "interval" } } } }
]
}SDK usage
await db.field.create({
data: {
databaseId,
tableId,
name: 'tags',
type: { name: 'citext', array_dimensions: 1 },
defaultValue: { value: [], cast: { name: 'citext', array_dimensions: 1 } },
isRequired: true,
},
select: { id: true, name: true, type: true },
}).execute();Validation
The validate_field_type and validate_field_default triggers on metaschema_public.field reject non-object inputs. Passing "type": "text" (a JSON string) returns:
FieldType must be an object, got stringAlways use the object format: "type": { "name": "text" }.
Database Provisioning (End-to-End)
Client Setup
import { createClient as createAuthClient } from '@constructive-db/sdk/auth';
import { createClient as createPublicClient } from '@constructive-db/sdk/public';
const authDb = createAuthClient({ endpoint: 'http://auth.localhost:3000/graphql' });
const publicDb = createPublicClient({ endpoint: 'http://api.localhost:3000/graphql' });Step 1: Sign Up + Sign In
await authDb.mutation.signUp({ input: { email, password } }, { select: { ok: true, errors: true } }).execute();
const signIn = await authDb.mutation.signIn(
{ input: { email, password } },
{ select: { result: { select: { accessToken: true, userId: true } } } }
).execute();
const { accessToken, userId } = signIn.signIn.result;Step 2: Provision Database
Always use modules: ['all'] and bootstrapUser: true:
publicDb.setHeaders({ Authorization: `Bearer ${accessToken}` });
const result = await publicDb.databaseProvisionModule.create({
data: {
databaseName: dbName,
ownerId: userId,
subdomain: dbName,
domain: 'localhost',
modules: ['all'],
bootstrapUser: true,
},
select: { id: true, databaseId: true, databaseName: true, status: true }
}).execute();
const dbId = result.createDatabaseProvisionModule?.databaseProvisionModule?.databaseId;Step 3: Apply Workarounds
See workarounds/fix-membership-defaults and workarounds/auto-verify-email.
Step 4: Per-DB Sign In
const dbAuth = createAuthClient({
endpoint: `http://auth-${dbName}.localhost:3000/graphql`
});
const dbSignIn = await dbAuth.mutation.signIn(
{ input: { email, password } },
{ select: { result: { select: { accessToken: true, userId: true } } } }
).execute();
const dbAccessToken = dbSignIn.signIn.result.accessToken;Step 5: Use Per-DB App API
import { createClient } from './generated/<db-name>/sdk/orm';
const db = createClient({
endpoint: `http://app-public-${dbName}.localhost:3000/graphql`,
headers: { Authorization: `Bearer ${dbAccessToken}` },
});
await db.notes.create({ data: { content: 'Hello' }, select: { id: true } }).execute();Module Reference
| Modules | What it installs |
|---|---|
['all'] | Everything — always use this for demos and real apps |
['uuid_module', 'users_module'] | Minimal — breaks app API auth |