Skip to content

Reference

Config schema

The shape of clivly.config.ts — the quickstart shape, a derived-object projection, and the sync trigger block.

Quickstart shape

The full clivly.config.ts produced by the five-minute path, wiring entities, a Drizzle source, and the sync trigger.

clivly.config.ts
import { createClivlySDK, defineClivlyConfig } from "clivly/sdk";
import { discoverFromDrizzle, fromDrizzle } from "clivly/drizzle";
import { db, participants, accounts } from "./db";
import * as schema from "./db/schema";

const entities = defineClivlyConfig({
entities: {
  participants: {
    concept: "contact",
    source: "participants",
    fields: { name: "full_name", email: "email" },
  },
  accounts: {
    concept: "company",
    source: "accounts",
    fields: { name: "legal_name", domain: "website" },
  },
},
});

export default createClivlySDK({
apiKey: process.env.CLIVLY_SECRET_KEY ?? process.env.CLIVLY_API_KEY!,
entities,
schema: discoverFromDrizzle(schema),
source: {
  contacts: fromDrizzle(db, participants, {
    entity: "participants",
    cursorField: "updatedAt",
  }),
  companies: fromDrizzle(db, accounts, {
    entity: "accounts",
    cursorField: "updatedAt",
  }),
},
syncTrigger: {
  path: "/api/clivly/tick",
  url: process.env.CLIVLY_SYNC_TRIGGER_URL,
  secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
},
// Long-lived Node hosts only. The HTTP heartbeat resumes automatically
// while a socket is connecting or reconnecting.
presence: { transport: "websocket" },
});

Cursor store

Required on serverless and edge runtimes. The default cursor store lives in memory, which a request-scoped runtime discards between invocations — so every delta sync starts from nothing and re-reads your entire source tables.

clivly.config.ts
import { createClivlySDK, kvCursorStore } from "@clivly/sdk";

export default createClivlySDK({
// ...
cursorStore: kvCursorStore(env.CLIVLY_CURSORS),
});

kvCursorStore accepts any key-value client with get and put — Workers KV, Upstash, node-redis. memoryCursorStore() is also exported, for tests and long-lived hosts that want the default named explicitly.

See Cursor stores for writing your own, and why SyncCursor.time being a Date is the one thing to get right.

Derived-object projection

An advanced custom entity is a custom entity with a projection. source is the base table; fields is derived from the bindings.

clivly.config.ts
// An advanced custom entity is a `custom` entity with a `projection`.
// `source` is the base table; `fields` is derived from the bindings.
client_accounts: {
concept: "custom",
source: "accounts",              // === projection.baseTable
objectType: { name: "Client account" },
projection: {
  version: 1,
  kind: "projection",
  entityKey: "client_accounts",
  baseTable: "accounts",
  joins: [
    {
      key: "cl",
      table: "clients",
      type: "inner",
      on: [
        {
          left: { table: "cl", column: "id" },
          op: "eq",
          right: { table: "base", column: "client_id" },
        },
      ],
    },
  ],
  fieldBindings: {
    client_name: { from: { table: "cl", column: "full_name" } },
    currency: { from: { table: "base", column: "currency" } },
  },
  identity: { kind: "base_pk" },
},
}

See Advanced custom entities for identity kinds, fan-out rules, and CI validation.

Sync trigger block

Lets remote sync call back into your app. path lets the SDK build its own public URL from the hosting platform's environment, so on those platforms you never set the URL by hand.

clivly.config.ts
syncTrigger: {
// `path` lets the SDK build its own public URL from the hosting platform's
// environment, so on those platforms you never set the URL by hand.
path: "/api/clivly/tick",
url: process.env.CLIVLY_SYNC_TRIGGER_URL,
secret: process.env.CLIVLY_SYNC_TRIGGER_SECRET,
},

Only POST triggers a sync. HEAD answers 204 and does no work — that is the liveness probe. GET still syncs but logs a deprecation warning, and a future release will refuse it with 405, so point any cron or uptime check at POST. See Only POST triggers a sync.

See Environment variables for what each variable does, and How Clivly reaches your app for how the URL resolves in development versus production.