Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt

Use this file to discover all available pages before exploring further.

/api/v1/schema-joins is the CRUD surface for SchemaJoin rows — declarative definitions that link two DataSchema tables on a primary-key/foreign-key pair. Joins drive customer-360 enrichment, segment overlap calculations, and reach-estimate queries that span more than one schema.

What it does

Each SchemaJoin row stores { primarySchemaId, primaryKey, foreignSchemaId, foreignKey, joinType, autoEnrich, status }. The route at src/app/api/v1/schema-joins/route.ts validates that both schemas exist and that the named key fields exist on each schema before persisting (route.ts:124-128). Invalid combinations are rejected with a 400 and a precise error message rather than allowed to fail at query time. The route handles four verbs:
  • GET — list all joins for the tenant, enriched with each schema’s displayName for the UI.
  • POST — create a new join after schema/field validation.
  • PUT — update fields on an existing join, re-validating any changed key combinations.
  • DELETE — remove a join (hard delete).

Quick start

Create a join from customers to accounts so customer reads automatically pull account fields:
curl -X POST https://playground.kaireonai.com/api/v1/schema-joins \
  -H "Content-Type: application/json" \
  -H "X-API-Key: krn_your_api_key" \
  -H "X-Tenant-Id: 5a9904b9-..." \
  -d '{
    "name": "Customer accounts",
    "primarySchemaId": "schema_customers",
    "primaryKey": "customer_id",
    "foreignSchemaId": "schema_accounts",
    "foreignKey": "customer_id",
    "joinType": "left",
    "autoEnrich": true
  }'

How it works

Authentication and roles

GET requires viewer, editor, or admin (route.ts:63). POST, PUT, and DELETE require editor or admin (route.ts:103, 154, 211). Every handler then calls requireTenant(request) and scopes all DB operations by tenantId — no cross-tenant joins are possible.

Field validation

validateSchemaField at route.ts:40-56 runs once per side on POST. It looks up the schema by (id, tenantId) and the named field by (schemaId, name). The error messages are precise:
  • Schema {id} not found — when the schema id does not exist for the tenant.
  • Field "{name}" not found in schema "{schemaName}" — when the schema exists but the named key column is missing.
On PUT, the validation only runs for sides whose primarySchemaId/primaryKey or foreignSchemaId/foreignKey actually changed (route.ts:186-193).

List enrichment

GET joins the result against prisma.dataSchema.findMany to attach primarySchemaName and foreignSchemaName (using displayName first, falling back to name, then "Unknown") so the UI does not need a second round trip per row (route.ts:74-90).

Reference

GET /api/v1/schema-joins

List all schema joins for the tenant, ordered by createdAt descending.

Response

Returned at route.ts:92.
[
  {
    "id": "join_001",
    "tenantId": "5a9904b9-...",
    "name": "Customer accounts",
    "primarySchemaId": "schema_customers",
    "primaryKey": "customer_id",
    "foreignSchemaId": "schema_accounts",
    "foreignKey": "customer_id",
    "joinType": "left",
    "autoEnrich": true,
    "status": "active",
    "createdAt": "2026-04-30T10:00:00.000Z",
    "updatedAt": "2026-04-30T10:00:00.000Z",
    "primarySchemaName": "Customers",
    "foreignSchemaName": "Accounts"
  }
]

Status codes

CodeWhen
200Returns the array (possibly empty)
401 / 403Caller fails authentication or role check
500Unexpected DB error

POST /api/v1/schema-joins

Create a new join after schema/field validation.

Request body

Validated by CreateSchemaJoinSchema at route.ts:13-21.
name
string
required
Display name. Must be at least 1 character.
primarySchemaId
string
required
Id of the primary DataSchema. Must belong to the tenant.
primaryKey
string
required
Field name on the primary schema. Must exist as a SchemaField row for primarySchemaId.
foreignSchemaId
string
required
Id of the foreign DataSchema. Must belong to the tenant.
foreignKey
string
required
Field name on the foreign schema. Must exist as a SchemaField row for foreignSchemaId.
joinType
string
default:"left"
"left" (default) or "inner".
autoEnrich
boolean
default:"true"
When true, customer-360 reads and enrichment stages auto-pull this join. When false, the join exists in the catalog but is opt-in per query.

Response

201 Created with the created SchemaJoin row.

Status codes

CodeWhenSource
201Createdroute.ts:143
400Invalid JSON bodyroute.ts:112
400Schema/field validation failure (with precise message)route.ts:117-128
401 / 403Caller fails authentication or role checkroute.ts:103-106
500Unexpected DB errorroute.ts:145

PUT /api/v1/schema-joins

Update fields on an existing join. Re-runs validation for any side whose schema or key changed.

Request body

Validated by UpdateSchemaJoinSchema at route.ts:23-33.
id
string
required
Id of the join to update.
name
string
New display name.
primarySchemaId
string
New primary schema id. When set, key validation re-runs against primaryKey.
primaryKey
string
New primary key field name.
foreignSchemaId
string
New foreign schema id. When set, key validation re-runs against foreignKey.
foreignKey
string
New foreign key field name.
joinType
string
"left" or "inner".
autoEnrich
boolean
Toggle the auto-enrichment flag.
status
string
"active" or "inactive". inactive joins stay in the catalog but are skipped by enrichment paths.

Response

200 OK with the updated SchemaJoin row.

Status codes

CodeWhenSource
200Updatedroute.ts:200
400Invalid JSON body or validation failureroute.ts:163, 167-170
401 / 403Caller fails authentication or role checkroute.ts:154-157
404Join not found for tenantroute.ts:178
500Unexpected DB errorroute.ts:202

DELETE /api/v1/schema-joins?id={joinId}

Hard-delete a join. The id is read from the query string, not the path (route.ts:216).

Query parameters

id
string
required
Id of the join to delete.

Response

{ "deleted": true }

Status codes

CodeWhenSource
200Deletedroute.ts:226
400Missing id query parameterroute.ts:217
401 / 403Caller fails authentication or role checkroute.ts:211-214
404Join not found for tenantroute.ts:222
500Unexpected DB errorroute.ts:228

Required headers

HeaderRequiredRead atPurpose
Content-TypePOST / PUT onlyNext.js frameworkapplication/json
X-API-KeyYes (one of the two)tenant.ts:97API key (krn_…)
X-Tenant-IdYes (one of the two)tenant.ts:113Direct tenant id

Honest limits

  • The route does not enforce a uniqueness constraint on { primarySchemaId, primaryKey, foreignSchemaId, foreignKey }. Operators can register multiple joins with the same key pair (e.g., one active and one inactive) — the consumer-side enrichment code picks the first active row it finds.
  • joinType is restricted to "left" and "inner" by the Zod schema (route.ts:19, 30). Right and outer joins are not supported in V1 — enforce them by inverting the primary/foreign relationship instead.
  • DELETE is unconditional — no FK guard prevents removing a join that an active enrichment stage relies on. Callers should check status: "inactive" for a phased deprecation rather than deleting in production.
  • The route uses query-string id for DELETE, not a path param, to allow batch tools to pass the id outside the URL path.
  • Schemas — register the DataSchema rows referenced by primarySchemaId and foreignSchemaId.
  • Customers — Unified Profile — consumes auto-enrich joins for customer-360 reads.
  • Segment Overlap — reads cross-schema joins to compute audience intersections.
  • Reach Estimate — uses joins for cross-schema rule reach calculations.