npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@newhomestar/sdk

v0.8.18

Published

Type-safe SDK for building Nova pipelines (workers & functions)

Readme

@newhomestar/sdk

Type-safe SDK for building Nova integrations, workers, and services.
Code-first: TypeScript + Zod is the single source of truth.
Build: nova integrations builddist/ + nova-integration.yaml + JSON schemas.
Push: nova integrations push → Docker image + full config sync to platform DB.

Version: 0.8.3
Runtime: Node 20 + esbuild CJS bundle
Schema system: Zod v4 → JSON Schema at build time


Table of Contents


Installation

yarn add @newhomestar/sdk zod
# or
npm install @newhomestar/sdk zod

Peer dependency: zod >= 4.0.0


Quick Start

import { defineIntegration, schema, event, action, runHttpServer } from "@newhomestar/sdk";
import { z } from "zod";

const Employee = schema("entity", z.object({
  id:    z.string(),
  email: z.string().email(),
  name:  z.string(),
}));

const integration = defineIntegration({
  slug: "my_provider",
  name: "My Provider",
  integrationType: "oauth2",
  queue: "my_provider_queue",
  baseUrl: "https://api.provider.com",
  authorizationEndpoint: "https://provider.com/authorize",
  tokenEndpoint: "https://provider.com/token",
  scopes: ["openid", "profile"],

  schemas: { employee: Employee },
  events: {
    employee_synced: event("outbound", { payload: Employee, category: "sync" }),
  },
  actions: {
    health: action({
      method: "GET", path: "/health",
      input: z.object({}),
      output: z.object({ ok: z.boolean() }),
      handler: async () => ({ ok: true }),
    }),
    listEmployees: action({
      method: "GET", path: "/employees",
      scopes: ["employee:read"],
      input: z.object({ limit: z.number().default(100) }),
      output: z.object({ employees: z.array(z.any()), total: z.number() }),
      handler: async (input, ctx) => {
        const creds = await ctx.resolveCredentials();
        const res = await ctx.fetch("https://api.provider.com/employees");
        const data = await res.json();
        return { employees: data.items, total: data.total };
      },
    }),
  },
});

export default integration;

// Runtime
runHttpServer(integration as any, { port: 8000 });

Export Paths

The SDK exposes three subpath exports:

| Import Path | Description | |---|---| | @newhomestar/sdk | Core: defineIntegration, action, schema, event, runHttpServer, runDualMode, runWorker, credential helpers | | @newhomestar/sdk/events | Events system: withServiceEventOutbox, startPollConsumer, startInboundConsumer, queueEvent, logEvent, isIntegrationSync | | @newhomestar/sdk/next | Next.js helpers (for service projects) |


Core Concepts

defineIntegration()

The central function that registers an integration definition — the single source of truth for both the containerized runtime AND the platform configuration.

import { defineIntegration } from "@newhomestar/sdk";

export default defineIntegration({
  // ── Identity ──
  slug: "bamboohr",              // snake_case, unique across platform
  name: "BambooHR",
  displayName: "BambooHR HRIS",  // shown in admin UI
  description: "...",

  // ── Classification ──
  integrationType: "oauth2",     // "oidc" | "oauth2" | "api_key"
  category: "hris",
  tags: ["hr", "employee-sync"],

  // ── Branding ──
  logoUrl: "https://...",
  color: "#73C41D",
  icon: "https://...",

  // ── OAuth/OIDC Endpoints ──
  authorizationEndpoint: "https://...",
  tokenEndpoint: "https://...",
  userinfoEndpoint: "https://...",       // optional
  revocationEndpoint: "https://...",     // optional
  jwksUri: "https://...",                // OIDC only
  baseUrl: "https://api.bamboohr.com/",
  scopes: ["openid", "profile"],

  // ── Container Runtime ──
  queue: "bamboohr_queue",
  resources: { cpu: "500m", memory: "512Mi" },
  envSpec: [
    { name: "NOVA_EVENTS_SERVICE_URL", secret: false },
    { name: "NOVA_SERVICE_TOKEN", secret: true },
    // ...
  ],

  // ── Configuration ──
  schemas:      { /* ... */ },
  events:       { /* ... */ },
  actions:      { /* ... */ },
  syncMappings: { /* ... */ },   // optional
  webhooks:     { /* ... */ },   // optional
});

7-Phase Normalization Pipeline

When defineIntegration() is called, it runs these phases in order:

| Phase | Description | |---|---| | 1. Normalize schemas | Fills slug and name on lean schema() results from their dictionary key (e.g., key employee → slug: "employee", name: "Employee") | | 2. Normalize events | Fills slug/name on lean event() results; resolves direct payload schema object references → string slugs for payloadSchema | | 3. Auto-extract functions | Every action is promoted to an IntegrationFunctionDef. HTTP method, path, scopes, capabilities are copied. __inputZod, __outputZod, __paramsMeta stored as non-enumerable props for the build step to convert to JSON Schema | | 4. Zod validation | Runs IntegrationDefSchema.parse(def) — structural validation of all fields | | 5. Cross-validate event schemas | Checks that every event.payloadSchema string reference exists in schemas | | 6. Cross-validate function schemas | Checks that requestSchema / responseSchema references exist in schemas | | 7. Validate webhook handler | Checks that webhooks.handler references an existing action |

IntegrationDef Field Reference

| Field | Required | Type | Description | |---|---|---|---| | slug | ✅ | string | Unique snake_case identifier (e.g., "bamboohr") | | name | ✅ | string | Human-readable name | | integrationType | ✅ | "oidc" \| "oauth2" \| "api_key" | Auth type | | queue | ✅ | string | PGMQ queue name for async processing | | baseUrl | ✅ | string | Base URL for external API calls | | actions | ✅ | Record<string, ActionDef> | Runtime action handlers | | schemas | ✅ | Record<string, IntegrationSchemaDef> | Zod schemas → JSON Schema on build | | events | ✅ | Record<string, IntegrationEventDef> | Event definitions | | description | | string | Short description | | category | | string | Grouping (e.g., "hris", "crm") | | logoUrl | | string | Logo for admin dashboard | | color | | string | Brand color hex code | | displayName | | string | Admin UI display name | | tags | | string[] | Discovery/filtering tags | | authorizationEndpoint | OAuth2/OIDC | string | OAuth authorize URL | | tokenEndpoint | OAuth2/OIDC | string | OAuth token URL | | userinfoEndpoint | | string | OIDC userinfo URL | | revocationEndpoint | | string | Token revocation URL | | jwksUri | OIDC recommended | string | JWKS URI for token verification | | scopes | | string[] | OAuth scopes to request | | resources | | { cpu, memory } | Container resource limits | | envSpec | | Array<{ name, secret, default? }> | Environment variable spec | | syncMappings | | Record<string, SyncMappingDef> | Field mapping rules | | webhooks | | WebhookConfig | Inbound webhook types | | functions | Auto-generated | Record<string, IntegrationFunctionDef> | Auto-extracted from actions (Phase 3) |


schema()

Lean helper to define Zod schemas. Slug and name are inferred from the key in schemas: {}.

import { schema } from "@newhomestar/sdk";
import { z } from "zod";

// slug = "employee", name = "Employee", schemaType = "entity"
const Employee = schema("entity", z.object({
  id:        z.string(),
  email:     z.string().email(),
  firstName: z.string(),
  lastName:  z.string(),
  status:    z.enum(["active", "inactive", "terminated"]),
}), {
  version: "1.0.0",
  description: "BambooHR employee record",
});

Schema Types

| Type | Use for | |---|---| | entity | Domain objects (employees, contacts, issues) | | request | API request body shapes | | response | API response shapes | | webhook_payload | Inbound webhook payload shapes | | configuration | Integration settings/config |

Verbose Alternative

import { integrationSchema } from "@newhomestar/sdk";

const Employee = integrationSchema({
  name: "Employee",
  slug: "employee",
  schemaType: "entity",
  schema: z.object({ /* ... */ }),
  version: "1.0.0",
});

event()

Lean helper to define integration events. Slug and name are inferred from the key.

import { event } from "@newhomestar/sdk";

const events = {
  // slug = "employee_synced", name = "Employee Synced"
  employee_synced: event("outbound", {
    payload: Employee,      // direct ref to schema() result (type-safe!)
    category: "sync",
    severity: "info",       // "info" | "warning" | "error" | "critical"
  }),
  
  webhook_received: event("inbound", {
    payload: WebhookPayload,
    category: "webhook",
  }),
  
  sync_failed: event("outbound", {
    severity: "error",
    category: "sync",
  }),
};

Event Directions

| Direction | Meaning | |---|---| | outbound | Integration → Platform (e.g., sync completed, data changed) | | inbound | Platform → Integration (e.g., webhook received) | | bidirectional | Both directions |

Outbound events are auto-registered in the platform event_types table when you run nova integrations push.


action()

The universal action builder. Every action is also auto-registered as a function in the platform DB.

import { action } from "@newhomestar/sdk";
import { z } from "zod";

const syncEmployees = action({
  // ── HTTP Routing ──
  method: "POST",                     // GET | POST | PUT | DELETE | PATCH
  path: "/employees/sync",

  // ── Zod I/O ──
  input: z.object({
    tenantId: z.string().uuid(),
    since: z.string().datetime().optional(),
  }),
  output: z.object({
    synced: z.number(),
    errors: z.number(),
  }),

  // ── Function Metadata ──
  name: "syncEmployees",              // optional, defaults to key name
  description: "Full employee sync",
  scopes: ["employee:read"],          // presence → auto-register as function
  category: "employees",

  // ── Parameter Metadata (for admin UI form builder) ──
  params: {
    tenantId: { in: "body", uiType: "uuid", label: "Tenant ID", required: true },
    since:    { in: "body", uiType: "datetime", label: "Since" },
  },

  // ── Triggers ──
  triggers: [
    { type: "schedule", cron: "0 */6 * * *", timezone: "UTC", description: "Every 6 hours" },
    { type: "event", events: ["hris.employee_updated"] },
  ],

  // ── Sync Metadata (for DataSync UI tab) ──
  sync: {
    entityType: "employee",
    direction: "to_nova",             // "to_nova" | "from_nova" | "bidirectional"
    label: "BambooHR Employees",
    description: "Import all employees from BambooHR",
  },

  // ── Expandable Relations (batch foreign-key resolution) ──
  expandable: {
    supervisor: {
      model: "employee",
      resolver: async (ids, ctx) => {
        // Batch-fetch supervisors by ID → Map<id, fullObject>
      },
    },
  },

  // ── Handler ──
  async handler(input, ctx) {
    ctx.progress(10, { status: "starting" });
    const res = await ctx.fetch("https://api.bamboohr.com/v1/employees/directory");
    const data = await res.json();
    ctx.progress(100, { status: "complete" });
    return { synced: data.employees.length, errors: 0 };
  },
});

action() Full Field Reference

| Field | Type | Description | |---|---|---| | method | string | HTTP method (default: "POST") | | path | string | Route path (default: /{workerName}/{actionName}) | | input | ZodType | Zod schema for input validation | | output | ZodType | Zod schema for output validation | | handler | (input, ctx) => Promise<output> | Action implementation | | name | string | Display name (default: key name) | | description | string | Human-readable description | | scopes | string[] | OAuth scopes → auto-registers as a platform function | | category | string | Grouping in the admin UI | | params | Record<string, ParamMeta> | Per-field metadata for path/query/body routing + UI hints | | triggers | Array<EventTrigger \| ScheduleTrigger> | Event subscriptions and cron schedules | | sync | { entityType, direction, label, description? } | DataSync tab metadata | | expandable | Record<string, { model, resolver }> | ?expand=field1,field2 batch resolvers | | capabilities | Array<Capability> | Legacy: webhook, scheduled, queue, stream triggers | | fga | { resourceType, relation, resourceIdKey? } | OpenFGA authorization hints | | events | string \| string[] | Shorthand for triggers: [{ type: 'event', events: [...] }] |


ActionCtx

Every action handler receives a context object (ctx) with these methods:

async handler(input: Input, ctx: ActionCtx) {
  // ── Job tracking ──
  ctx.jobId;                          // unique job ID (e.g., "http-1234567890")
  ctx.progress(50, { step: "fetching" });  // report progress to platform

  // ── Authentication ──
  ctx.authToken;                      // raw Bearer token from inbound request
  ctx.auth;                           // validated JWT payload (sub, iss, exp, aud, etc.)

  // ── Credential Resolution ──
  const creds = await ctx.resolveCredentials();
  // OR: const creds = await ctx.resolveCredentials("other_integration", userId);

  // ── mTLS-aware Fetch (with 401 auto-retry) ──
  const res = await ctx.fetch("https://api.provider.com/data");
  // OR: const res = await ctx.fetch(url, { method: "POST", body: "..." }, creds);

  // ── HTTP headers (HTTP mode only) ──
  ctx.headers;                        // raw request headers

  // ── SSE worker mode only ──
  ctx.read_ct;                        // message delivery count
  await ctx.heartbeat?.(30);          // extend visibility timeout by 30s
}

ctx.resolveCredentials() Flow

  1. Calls GET {AUTH_ISSUER_BASE_URL}/api/integrations/{slug}/credentials with the inbound JWT
  2. Auth server decrypts credentials from Vault and returns them
  3. SDK performs OAuth token exchange locally (client_credentials or mTLS)
  4. Returns ResolvedCredentials with accessToken, expiresAt, authMode, httpsAgent

ctx.fetch() 401 Auto-Retry

  1. Makes the request with current accessToken
  2. If 401 → sends X-Nova-Token-Invalid: true to auth server → gets fresh credentials
  3. If the new token is different → retries once automatically
  4. If same token → returns original 401 (prevents infinite loops)

Running the Integration

runHttpServer()

Starts an Express HTTP server with one route per action.

import { runHttpServer } from "@newhomestar/sdk";

runHttpServer(integration as any, {
  port: 8000,
  issuerBaseURL: "https://auth.newhomeconnect.dev",
  audience: "starfleet",
  publicPaths: ["/webhooks"],   // exempt from JWKS auth
  skipAuth: false,              // or set NOVA_SKIP_AUTH=true
});

Features:

  • JWKS JWT authentication via express-oauth2-jwt-bearer (RS256)
  • /health and /healthcheck are auto-exempted from auth
  • Smart input extraction: params metadata → path/query/body/header; type coercion for query strings
  • ?expand=field1,field2 — batch foreign-key resolution via expandable config

runDualMode()

Starts both HTTP server and queue consumer concurrently.

import { runDualMode } from "@newhomestar/sdk";

runDualMode(integration as any, { port: 8000 });
// HTTP API + SSE/pgmq queue consumer in background

⚠️ Warning: If you also use startPollConsumer() for inbound event processing, use runHttpServer() instead of runDualMode() to avoid competing consumers on the same queue.

runWorker()

Queue-only mode. Automatically selects:

  • SSE mode (preferred): when NOVA_EVENTS_SERVICE_URL + NOVA_SERVICE_TOKEN are set
  • Legacy pgmq mode: when RUNTIME_SUPABASE_* env vars are set

Which Runtime Mode to Use

| Scenario | Recommended Mode | |---|---| | Integration with HTTP API + separate poll consumer | runHttpServer() + startPollConsumer() | | Integration with HTTP API + built-in SSE consumer | runDualMode() | | Pure queue worker (no HTTP) | runWorker() | | Background/headless sync loops | createIntegrationClient() + setImmediate(processLoop) |


Events System

Import from @newhomestar/sdk/events:

import {
  withServiceEventOutbox,
  withEventOutbox,
  withWebhookEvent,
  queueEvent,
  logEvent,
  startOutboxRelay,
  startInboundConsumer,
  startPollConsumer,
  isIntegrationSync,
  NovaEventsClient,
} from "@newhomestar/sdk/events";

Outbound Events (Producer)

withServiceEventOutbox() — Preferred

Atomic: data write + outbox row in one Prisma $transaction, then best-effort relay. Auto-stamps x-source/x-integration-id from request headers.

const employee = await withServiceEventOutbox(db, req, async (tx, emit) => {
  const row = await tx.hrisEmployee.update({ where: { id }, data });
  emit("employee.updated", { id: row.id, firstName: row.firstName });
  return row;
});

withEventOutbox() — Legacy

Same atomicity but with a { events, result } return shape:

const employee = await withEventOutbox(db, async (tx) => {
  const row = await tx.hrisEmployee.update({ where: { id }, data });
  return {
    events: [{ entity_type: "employee", action: "updated", entity_id: id }],
    result: row,
  };
});

queueEvent() — Stateless

No transaction context. Directly POSTs to the Events Service.

await queueEvent({
  event_slug: "bamboohr.employee_synced",
  source_service: "bamboohr",
  attributes: { synced: 150, errors: 0 },
});

logEvent() — Audit Only

Records an event in the audit log without queue fan-out.

await logEvent({
  event_slug: "bamboohr.employee_viewed",
  entity_id: employeeId,
  performed_by_id: userId,
});

Inbound Events (Consumer)

startPollConsumer() — Recommended for DO App Platform

HTTP pull-based consumer. No persistent connections or proxy timeout issues.

const consumer = startPollConsumer(db, {
  queueName: "bamboohr_queue",
  batch: 5,         // messages per poll
  vt: 60,           // visibility timeout (seconds)
  idleDelayMs: 2000, // sleep when queue is empty
  handlers: {
    "hris.employee_created": async (tx, event) => {
      if (isIntegrationSync(event.payload)) return { status: "skipped" };
      await tx.bambooEmployee.upsert({ /* ... */ });
      return { status: "processed" };
    },
    "hris.employee_updated": async (tx, event) => {
      // ...
      return { status: "processed" };
    },
  },
  defaultHandler: async (_tx, event) => {
    console.warn(`Unhandled event: ${(event.payload as any).topic}`);
    return { status: "skipped" };
  },
});

// Graceful shutdown
process.on("SIGTERM", () => consumer.abort());

startInboundConsumer() — SSE-Based

Long-lived SSE connection. Best for non-DO deployments.

const consumer = startInboundConsumer(db, {
  queueName: "bamboohr_queue",
  handlers: { /* same as poll consumer */ },
  vt: 30,
  maxReconnectDelay: 30_000,
});

Webhook ACID Processing

withWebhookEvent()

Atomically processes inbound webhooks with idempotency:

const { status } = await withWebhookEvent(db, {
  idempotencyKey: `bamboohr:${webhookId}:${timestamp}`,
  eventType: "bamboohr.employee.updated",
  queueName: "bamboohr_webhooks",
  payload: rawBody,
}, async (tx, emit) => {
  await tx.bambooEmployee.upsert({ /* ... */ });
  emit("employee.upserted", { id: employee.id });
});
// Returns { status: "processed" } or { status: "duplicate" }

ACID guarantee: idempotency check → inbound_events INSERT → handler → outbox rows → mark processed — all in one $transaction. Rolls back entirely on failure.

Outbox Relay

Start once at service boot:

startOutboxRelay(db, {
  intervalMs: 60_000,   // poll every 60s
  maxAttempts: 5,       // give up after 5 attempts
});

Retry schedule (exponential backoff): 2^attempts × 5s → 5s, 10s, 20s, 40s, 80s.

Echo Loop Prevention

import { isIntegrationSync } from "@newhomestar/sdk/events";

// In a consumer handler:
if (isIntegrationSync(event.payload)) {
  return { status: "skipped" }; // Don't write back to the system that caused this event
}

Checks for x-source: integration_sync in headers or metadata.source === "integration_sync" in event payloads.

Topic Format Convention

{source_service}.{entity}_{action}

Examples:

  • hris.employee_created
  • bamboohr.user_synced
  • nova_ticketing_service.ticket_updated

Credential Resolution

HTTP Callback Strategy

The only credential resolution strategy. No direct database access needed.

Container ──JWT──> Auth Server ──Vault──> Decrypted Credentials
                   (AUTH_ISSUER_BASE_URL)

Flow:

  1. Container receives request with JWT (validated by JWKS middleware)
  2. Action calls ctx.resolveCredentials()
  3. SDK calls GET {AUTH_ISSUER_BASE_URL}/api/integrations/{slug}/credentials with Authorization: Bearer {jwt}
  4. Auth server verifies JWT, decrypts credentials from Vault, returns them
  5. SDK performs local OAuth token exchange (client_credentials / mTLS)
  6. Container uses the accessToken to call the integration API

ctx.resolveCredentials() and ctx.fetch()

// Auto-resolve for the current integration
const creds = await ctx.resolveCredentials();

// Resolve for a different integration
const creds = await ctx.resolveCredentials("other_integration", userId);

// ctx.fetch() — resolves credentials automatically + 401 auto-retry
const res = await ctx.fetch("https://api.provider.com/employees");

// ctx.fetch() with explicit credentials
const res = await ctx.fetch(url, { method: "POST", body: "..." }, creds);

Auth modes returned by ResolvedCredentials:

  • standard — authorization_code (per-user, pre-resolved access token)
  • client_credentials — server-to-server OAuth (SDK performs token exchange)
  • mtls — client_credentials + mTLS cert/key (SDK uses node:https agent)

createIntegrationClient()

For background/headless sync loops that don't have an inbound JWT:

import { createIntegrationClient } from "@newhomestar/sdk";

const client = createIntegrationClient("bamboohr");
// Resolves via NOVA_SERVICE_TOKEN (not user JWT)

const res = await client.fetch("https://api.bamboohr.com/v1/employees/directory");
const data = await res.json();

// Auto-refreshes credentials when they expire (60s buffer)
// Auto-retries on 401 with fresh credentials

emitPlatformEvent()

Emit PGMQ events to the platform database for cross-service communication:

import { createPlatformClient, emitPlatformEvent } from "@newhomestar/sdk";

const platformDB = createPlatformClient();
await emitPlatformEvent(platformDB, "employee.sync_complete", "bamboohr", {
  tenantId, synced: 150, errors: 0,
});

Parameter Metadata (ParamMeta)

Per-field metadata that tells the platform where each input field goes (path, query, body, header) and what UI widget the admin dashboard should render.

params: {
  id:       { in: "path",  uiType: "text",   label: "Employee ID", required: true },
  asOfDate: { in: "query", uiType: "date",   label: "As-of Date", placeholder: "YYYY-MM-DD" },
  syncType: { in: "body",  uiType: "select", label: "Sync Type", options: [
    { label: "Full", value: "full" },
    { label: "Incremental", value: "incremental" },
  ]},
  limit:    { in: "query", uiType: "number", label: "Limit", defaultValue: 100, min: 1, max: 500 },
}

ParamMeta Field Reference

| Field | Type | Description | |---|---|---| | in | "path" \| "query" \| "body" \| "header" | Where the parameter goes in the HTTP request | | uiType | ParamUiType | UI widget: text, textarea, number, integer, boolean, date, datetime, select, multiselect, password, email, url, uuid, json, hidden | | label | string | Human-readable label | | description | string | Help text below the input | | placeholder | string | Placeholder text | | required | boolean | Overrides Zod's optional/required | | defaultValue | unknown | Default value in UI | | options | Array<{ label, value }> | For select/multiselect | | min / max | number | Range for numbers, length for strings | | step | number | Increment for number inputs | | pattern | string | Regex for frontend validation | | order | number | Display priority (lower = higher) | | group | string | Visual grouping (e.g., "Identity", "Options") |

Convention fallback (when params not provided):

  • Fields matching :param in path → path params
  • Remaining fields for GET → query params
  • Remaining fields for POST/PUT/PATCH → body params
  • UI type inferred from Zod type (string→text, number→number, boolean→boolean, enum→select)

Sync Mappings

Declare how integration entities map to Nova service schemas. Seeded into integration_sync_pairs + integration_field_mappings on push.

syncMappings: {
  employee: {
    service: "hris",
    targetSchema: "employee",
    direction: "integration_to_service",
    fields: [
      { source: "workEmail",  target: "work_email" },
      { source: "firstName",  target: "first_name" },
      { source: "lastName",   target: "last_name" },
      { source: "status", target: "employment_status",
        transform: "status = 'Active' ? 'ACTIVE' : 'INACTIVE'" },  // JSONata
      { source: "supervisor", sourcePath: ["supervisor", "id"], target: "manager_id" },
    ],
  },
  time_off_request: {
    service: "hris",
    targetSchema: "time_off_request",
    direction: "integration_to_service",
    fields: [ /* ... */ ],
  },
},

Webhook Configuration

Declares inbound webhook types the provider supports:

webhooks: {
  handler: "handleWebhook",   // must reference an existing action key
  types: {
    employee_changes: {
      label: "Employee Changes",
      description: "Fires when employee records are created, updated, or deleted",
      produces: ["bamboohr.employee.created", "bamboohr.employee.updated", "bamboohr.employee.deleted"],
      authentication: {
        method: "hmac_sha256",
        signatureHeader: "X-BambooHR-Signature",
        secretSource: "platform_generated",
      },
      fields: [
        { name: "events", label: "Event Types", type: "multiselect", required: true,
          options: [
            { label: "Created", value: "created" },
            { label: "Updated", value: "updated" },
            { label: "Deleted", value: "deleted" },
          ]},
      ],
    },
  },
},

Synced to app_webhook_types on push. The Odyssey UI renders configuration forms from the fields array.


Dual Database Pattern

Each integration connects to two databases:

┌─────────────────────────┐     ┌────────────────────────────┐
│   Platform DB            │     │   Integration DB             │
│   (read-only)            │     │   (read-write)               │
│                          │     │                              │
│  • OAuth tokens          │     │  • domain tables             │
│  • Tenant config         │     │  • sync jobs                 │
│  • PGMQ queues           │     │  • webhook events            │
│                          │     │  • inbound/outbound events   │
│  PLATFORM_SUPABASE_URL   │     │  INTEGRATION_SUPABASE_URL    │
│  getPlatformClient()     │     │  getIntegrationClient()      │
└─────────────────────────┘     └────────────────────────────┘
import { createClient, type SupabaseClient } from "@supabase/supabase-js";

function getPlatformClient(): SupabaseClient {
  return createClient(
    process.env.PLATFORM_SUPABASE_URL!,
    process.env.PLATFORM_SUPABASE_SERVICE_ROLE_KEY!,
    { auth: { autoRefreshToken: false, persistSession: false } }
  );
}

function getIntegrationClient(): SupabaseClient {
  return createClient(
    process.env.INTEGRATION_SUPABASE_URL!,
    process.env.INTEGRATION_SUPABASE_SERVICE_ROLE_KEY!,
    { auth: { autoRefreshToken: false, persistSession: false } }
  );
}

nova-integration.yaml Spec

Generated by nova integrations build. Full annotated example:

apiVersion: nova.dev/v1
kind: Integration
metadata:
  slug: bamboohr
  name: BambooHR
  displayName: BambooHR HRIS
  description: Connects BambooHR HRIS to Nova
  category: hris
  tags: [hr, employee-sync]
  logoUrl: https://...
  color: "#73C41D"
spec:
  type: oauth2                          # oidc | oauth2 | api_key
  endpoints:
    authorization: https://bamboohr.com/authorize.php
    token: https://bamboohr.com/token.php
    baseUrl: https://api.bamboohr.com/api/gateway.php/newhomestar
  scopes: [employee, time_off, time_tracking]   # merged from all action scopes
  runtime:
    type: integration
    image: registry.digitalocean.com/nhc/bamboohr:main
    queue: bamboohr_queue
    command: [node, dist/index.cjs]
    resources: { cpu: 500m, memory: 512Mi }
    envSpec:
      - { name: NOVA_EVENTS_SERVICE_URL, secret: false }
      - { name: NOVA_SERVICE_TOKEN, secret: true }
  actions:
    - name: syncEmployees
      async: true
      triggers:
        - { type: schedule, cron: "0 */6 * * *", timezone: UTC }
      scopes: [employee:read]
      sync:
        entityType: employee
        direction: to_nova
        label: BambooHR Employees
      input: { ... }     # JSON Schema (converted from Zod)
      output: { ... }
      schema:
        input: ./schemas/syncEmployees.input.json
        output: ./schemas/syncEmployees.output.json
  schemas:
    - slug: employee
      name: Employee
      type: entity
      schema: { type: object, properties: { ... } }
      version: "1.0.0"
      fieldCount: 42
  events:
    - slug: employee_synced
      name: Employee Synced
      direction: outbound
      category: sync
      severity: info
      payloadSchema: employee
  functions:                            # auto-extracted from actions
    - slug: sync_employees
      name: Sync Employees
      httpMethod: POST
      endpointPath: /employees/sync
      requiredScopes: [employee:read]
      category: employees
  syncMappings:
    - sourceEntity: employee
      service: hris
      targetSchema: employee
      direction: integration_to_service
      fields:
        - { source: workEmail, target: work_email }
        - { source: status, target: employment_status,
            transform: "status = 'Active' ? 'ACTIVE' : 'INACTIVE'" }
  webhookTypes:
    - slug: employee_changes
      label: Employee Changes
      produces: [bamboohr.employee.created, bamboohr.employee.updated]
      handler: handle_webhook
build:
  dockerfile: ./Dockerfile
  context: .
ui:
  category: hris
  color: "#73C41D"

CLI Reference

nova integrations new

Scaffolds a new integration project.

nova integrations new [directory]

Interactive prompts:

  1. Integration slug (snake_case) — if no directory arg
  2. Integration type: OIDC, OAuth2, or API Key

Generates: | File | Purpose | |---|---| | src/index.ts | Full starter integration with defineIntegration(), schemas, events, actions, poll consumer | | src/lib/db.ts | Prisma singleton client | | package.json | @newhomestar/[email protected], zod, prisma | | prisma/schema.prisma | Baseline Prisma schema with outbox models | | Dockerfile | Distroless Node 20 container | | tsconfig.json | TypeScript configuration | | Agents.md | LLM agent instructions | | supabase/ | Local Supabase config + migrations directory |

Post-scaffold: runs git init, corepack enable, yarn install

Optional AI assist: generates starter schemas/events from a plain-English description using OpenAI.

nova integrations build

Bundles the integration and generates all build artifacts.

nova integrations build [-o dist] [--dry-run] [--skip-bundle]

8-Step Pipeline:

| Step | What happens | |---|---| | 1. Pass 1 esbuild | Bundles src/index.tsdist/index.build.cjs with zod external (so the CLI can use the integration's Zod for instanceof checks) | | 2. Dynamic import | Loads the build-time bundle, finds export default defineIntegration({...}) | | 3. Validate | Runs validateIntegration() — checks slug, name, schemas, baseUrl, actions | | 4. Load Zod v4 | From integration's node_modules; uses z.toJSONSchema() for schema conversion | | 5. Convert schemas | Each schemas.* entry → JSON Schema file at dist/schemas/{slug}.json | | 6. Convert events + functions | Extracts metadata; converts __inputZod/__outputZodinputSchema/outputSchema JSON | | 7. Convert actions | Zod I/O → JSON Schema; serializes triggers, scopes, sync blocks | | 8. Pass 2 esbuild | Production bundle with ignoreAnnotations: true (zod fully inlined, zero node_modules at runtime) |

Scope rollup: auto-collects all scopes from all actions, extracts base names (employee:reademployee), merges with top-level def.scopes.

Output:

  • dist/index.cjs — production bundle (~50MB image in distroless Docker)
  • dist/schemas/*.json — JSON Schema files per entity and action I/O
  • nova-integration.yaml — the full spec document

nova integrations push

Builds, pushes Docker image, and syncs all configuration to the platform.

nova integrations push [--skip-build] [--skip-secrets-fetch] [-d|--destructive] [--tag <t>] [--registry <r>]

11-Step Pipeline:

| Step | Description | |---|---| | 1. Fetch secrets | POST {odysseyUrl}/api/secrets using stored CLI session token | | 2. Build integration | Runs buildIntegration() → spec + nova-integration.yaml | | 3. Docker registry login | doctl registry login (for DigitalOcean registry) | | 4. Docker build | docker build --platform linux/amd64 -t {registry}/{slug}:{tag} . | | 5. Docker push | docker push {image} | | 6. Register container | POST /api/integrations — metadata, image, queue, actions | | 7. Sync config | POST /api/integrations/config — upsert schemas, events, functions | | 8. Seed sync mappings | POST /api/integrations/sync-mappingsintegration_sync_pairs + integration_field_mappings | | 8b. Sync entity types | POST /api/integrations/sync-entities — DataSync tab entries from action.sync blocks | | 8c. Sync webhook types | POST /api/integrations/webhook-typesapp_webhook_types rows | | 9. Register outbound events | POST /api/integration-events/sync + POST {NOVA_EVENTS_SERVICE_URL}/event-types | | 10. Register triggers | Ensure PGMQ queue exists; upsert event_subscriptions for each trigger topic | | 11. Sync trigger registry | POST /api/event-triggers/sync — platform DB for UI display |

Tag defaulting: current git branch name (sanitized to [a-z0-9.-])

--destructive flag: deletes all existing endpoints/schemas/events before upserting (guarantees clean slate).


Environment Variables

Runtime (Container)

| Variable | Secret | Required By | |---|---|---| | NOVA_EVENTS_SERVICE_URL | No | startPollConsumer, queueEvent, withEventOutbox | | NOVA_SERVICE_TOKEN | Yes | Events Service auth, createIntegrationClient() | | AUTH_ISSUER_BASE_URL | No | ctx.resolveCredentials() — auth server URL | | AUTH_AUDIENCE | No | JWKS audience claim (default: "starfleet") | | PLATFORM_SUPABASE_URL | No | createPlatformClient(), emitPlatformEvent() | | PLATFORM_SUPABASE_SERVICE_ROLE_KEY | Yes | Platform DB service role | | INTEGRATION_SUPABASE_URL | No | Integration's own database | | INTEGRATION_SUPABASE_SERVICE_ROLE_KEY | Yes | Integration DB service role | | NOVA_SERVICE_SLUG | No | Stamped as source_service on outbox events | | PORT | No | HTTP server port (default: 8000) | | NOVA_SKIP_AUTH | No | Set "true" to disable JWKS auth (dev only) | | DATABASE_URL | Yes | Prisma connection string |

CLI / Build

| Variable | Secret | Used By | |---|---|---| | DIGITALOCEAN_ACCESS_TOKEN | Yes | Docker registry login (nova integrations push) | | NPM_TOKEN | Yes | Docker build arg for private npm packages | | GIT_SHA | No | Docker image tag fallback | | NOVA_DOCKER_REGISTRY | No | Override registry prefix (default: registry.digitalocean.com/nhc) | | ODYSSEY_UI_URL | No | Override Odyssey UI URL (default from CLI config) | | OPENAI_API_KEY | Yes | AI-assisted scaffolding (nova integrations new) |


Validation Rules

validateIntegration() runs automatically during nova integrations build. Here's what it checks:

Errors (block build/push)

| Check | Condition | |---|---| | slug required | Must be present and non-empty | | name required | Must be present and non-empty | | queue required | Must be present and non-empty | | baseUrl required | Must be present and non-empty | | At least 1 action | actions must have ≥1 entry | | OAuth endpoints | authorizationEndpoint + tokenEndpoint required for oauth2/oidc |

Warnings (non-blocking)

| Check | Condition | |---|---| | No scopes | OAuth2/OIDC should have ≥1 scope | | Missing OIDC fields | jwksUri, userinfoEndpoint recommended for OIDC | | Non-HTTPS URLs | All endpoint URLs should use HTTPS (except localhost) | | No schemas | At least one entity schema recommended | | No health action | health or healthCheck action recommended | | No envSpec | Environment variables won't be validated at deploy time |


Architecture Diagram

src/index.ts
  └─ defineIntegration({
        schemas: { employee: schema("entity", EmployeeZod) }
        events:  { employee_synced: event("outbound", ...) }
        actions: { syncEmployees: action({ scopes, triggers, sync, handler }) }
     })
        │
        │  nova integrations build
        ▼
  dist/index.cjs       (production bundle — runs in distroless Docker)
  dist/schemas/        (JSON Schema files)
  nova-integration.yaml (spec document)
        │
        │  nova integrations push
        ▼
  ┌─────────────────────────────────────────────────────────────────┐
  │  Docker Registry    registry.digitalocean.com/nhc/{slug}:{tag}  │
  ├─────────────────────────────────────────────────────────────────┤
  │  Platform DB        app_integrations                            │
  │                     app_integration_schemas                     │
  │                     app_integration_events                      │
  │                     app_integration_functions                   │
  │                     integration_sync_pairs                      │
  │                     integration_field_mappings                  │
  │                     app_webhook_types                           │
  │                     app_integration_sync_entities               │
  ├─────────────────────────────────────────────────────────────────┤
  │  Events Service     event_types (outbound event registration)   │
  │                     event_subscriptions (PGMQ queue routing)    │
  ├─────────────────────────────────────────────────────────────────┤
  │  Orchestrator       Container registered, queue ensured         │
  └─────────────────────────────────────────────────────────────────┘