Developer Documentation

SDK Reference

@veltrixsecops/app-sdk is the typed contract between your app and the Veltrix platform. It defines the handler contexts, result shapes, manifest types, and canonical-layout helpers your app compiles against. The platform constructs every context at runtime; your app only consumes them.

Types-first and React-free at the root

The SDK is a types-first, side-effect-free package: almost everything you use is a type imported with import type, so it disappears at compile time. Pipeline handlers run in a hardened Node process and must never transitively require React — so the SDK's root entry is React-free (this landed in 2.0.0). React lives on subpaths: import hooks from @veltrixsecops/app-sdk/hooks and UI from @veltrixsecops/app-sdk/ui; import handler types/helpers from @veltrixsecops/app-sdk. (There is no separate @veltrixsecops/ui package.)

Install & Import

Add the SDK as a dev dependency (it is a compile-time contract; released packages bundle nothing from it at runtime):

npm install --save-dev @veltrixsecops/app-sdk typescript

The package exposes eight entry points (subpaths); the root is React-free, the UI/hooks/connections/byol subpaths require React:

// Root (React-free): handler types + define* helpers + layout constants
import type { PipelineContext, DeployContext, ValidationResult } from '@veltrixsecops/app-sdk'
import { APP_LAYOUT, HANDLER_NAMES, conventionalPaths } from '@veltrixsecops/app-sdk'

import { defineDeployer, defineConnectionTester } from '@veltrixsecops/app-sdk/pipeline'  // handler helpers
import { useAppContext, usePermissions } from '@veltrixsecops/app-sdk/hooks'               // React hooks
import { authFetch } from '@veltrixsecops/app-sdk/client'                                  // browser client
import { Button, Card } from '@veltrixsecops/app-sdk/ui'                                   // UI kit
import { ConnectionsManager } from '@veltrixsecops/app-sdk/connections'                    // Settings → Connections
// also: /byol (Bring-Your-Own-Infra console) and /opentofu (generic IaC provisioning)

The one hard rule

Everything a handler needs arrives on its context. Never import @prisma/client, reach into server/src/..., or use a relative import that escapes your app directory. CI rejects imports that break these boundaries. Read platform data through ctx.platform; touch your own tables through ctx.db in hooks and routes.

Handler Contexts

All five pipeline contexts extend a common PipelineContext. The deployment-oriented contexts add a target component plus credential and connectivity references.

PipelineContext (base)

Passed to validate and getStatus; the base for every other context.

interface PipelineContext {
  appId: string
  customerId: string
  configTypeId: string
  canvas: CanvasSnapshot              // the configuration being acted on (sections + snapshot)
  environment: EnvironmentRef         // { id, name }
  user: UserRef                       // { id, email, name }
  settings: Record<string, unknown>   // admin-configured app settings for this tenant
  platform: PlatformDataApi           // read-only, tenant-scoped platform data (see below)
  identity?: IdentityBroker           // for brokered (consent-onboarded) connections — mint an app-only token
  component?: ComponentRef | null     // present so validate can do live checks against the target
  credential?: CredentialRef | null
}

DeployContext

Passed to deploy. Runs per target component; the engine handles targeting and strategy.

interface DeployContext extends PipelineContext {
  component: ComponentRef                         // target host: { id, hostname, port, type[], toolId }
  credential: CredentialRef | null                // tool auth: username/password/apiToken/certificate
  connectivity: ConnectivityRef | null            // legacy per-component access (SSH/HTTPS/Tailscale IP)
  connectivityProvider: ConnectivityProviderRef | null  // preferred: platform-managed provider + config
  previousConfig: CanvasSnapshot | null           // the last successfully deployed config, if any
  remote?: RemoteExecutor                          // managed-ZTNA only: place files + run allow-listed intents over the tenant's tailnet
  strategy: DeploymentStrategy                     // 'DIRECT' | 'CANARY' | 'BLUE_GREEN' | 'ROLLING'
  canaryPercent?: number                           // present for CANARY
}

RollbackContext

Passed to rollback when a deployment fails or an operator reverts.

interface RollbackContext extends PipelineContext {
  component: ComponentRef
  credential: CredentialRef | null
  connectivity: ConnectivityRef | null
  connectivityProvider: ConnectivityProviderRef | null
  remote?: RemoteExecutor            // managed-ZTNA only (same as DeployContext)
  rollbackData: unknown              // whatever your deploy() returned as rollbackData
  targetVersion: CanvasSnapshot      // the canvas snapshot to revert to
}

HealthCheckContext

Passed to healthCheck during and after a deployment.

interface HealthCheckContext extends PipelineContext {
  component: ComponentRef
  credential: CredentialRef | null
  connectivity: ConnectivityRef | null
  connectivityProvider: ConnectivityProviderRef | null
}

DriftContext

Passed to the optional driftDetect handler on a schedule.

interface DriftContext extends PipelineContext {
  component: ComponentRef
  credential: CredentialRef | null
  connectivity: ConnectivityRef | null
  connectivityProvider: ConnectivityProviderRef | null
  remote?: RemoteExecutor            // managed-ZTNA only: read live files/hashes off the target
  deployedConfig: CanvasSnapshot     // what the pipeline last deployed — compare against live state
}

Result Types

Each handler returns a specific, typed result. These are the shapes the pipeline engine understands.

interface ValidationResult {
  valid: boolean
  errors: ValidationError[]          // { field, message, code } — block approval
  warnings: ValidationWarning[]      // { field, message, code } — shown, non-blocking
}

interface DeployResult {
  success: boolean
  message: string
  artifacts?: Record<string, unknown>
  rollbackData?: unknown             // handed back to rollback() if a revert is needed
}

interface RollbackResult { success: boolean; message: string }

interface HealthCheckResult {
  healthy: boolean
  score: number                      // 0–100; a low score can trigger auto-rollback
  checks: HealthCheck[]              // { name, passed, message, latencyMs? }
}

interface DriftResult {
  hasDrift: boolean
  diffs: DriftDiff[]                 // { field, expected, actual, severity: 'info'|'warning'|'critical',
                                     //   actor? } — actor is best-effort "who changed it + when",
                                     //   resolved from the tool's own audit log (DriftActor)
}

interface ConfigStatus {
  deployed: boolean
  version: string
  lastDeployedAt: string
  componentStatuses: ComponentConfigStatus[]   // per-component deployed/version/health
}

ctx.platform — Platform Data API

Every handler context carries ctx.platform: a tenant-scoped, read-only accessor for platform records. It is the only supported way to read platform data from an app.

interface PlatformDataApi {
  /** Latest deployment for a canvas, optionally filtered by status (e.g. 'SUCCEEDED'). */
  getLatestDeployment(
    canvasId: string,
    opts?: { status?: string },
  ): Promise<DeploymentSummary | null>

  /** Components for the current customer, optionally filtered by component types. */
  listComponents(filter?: { types?: string[] }): Promise<ComponentRef[]>
}
export default async function getStatus(ctx: PipelineContext): Promise<ConfigStatus> {
  const last = await ctx.platform.getLatestDeployment(ctx.canvas.canvasId, { status: 'SUCCEEDED' })
  const indexers = await ctx.platform.listComponents({ types: ['indexer'] })
  // ...build ConfigStatus from real deployment + component data
}

Never query the platform database directly

ctx.platform is the stable, scoped surface. Importing @prisma/client or reading platform tables directly is unsupported, breaks tenant isolation, and is rejected by CI.

Hook & Route Contexts

Apps can also run lifecycle hooks and expose HTTP routes. Both receive a loosely-typed handle to the platform database (ctx.db) scoped to your app's own prefixed tables.

AppHookContext

Passed to onInstall / onUninstall (and onEnable / onDisable / onUpgrade). customerId is present only for customer-scoped hooks.

import type { AppHookContext } from '@veltrixsecops/app-sdk'

export default async function onInstall(ctx: AppHookContext): Promise<void> {
  // ctx.db — platform database client; touch only your own app_<prefix>_ tables
  // ctx.appId, ctx.customerId?
  await ctx.db.$executeRawUnsafe(`SELECT 1`)
}

AppRouteContext

Passed to your server.entry module, which the platform mounts as a Fastify plugin under /api/apps/<appId> with auth and app-enabled checks already applied.

import type { AppRouteContext } from '@veltrixsecops/app-sdk'

export default async function routes(fastify: any, ctx: AppRouteContext) {
  // ctx.appId, ctx.appDir, ctx.manifest, ctx.db, ctx.events, ctx.hasPermission, ctx.resolveConnection
  fastify.get('/indexes', {
    preHandler: [ctx.hasPermission('indexes', 'read')],   // app-scoped permission guard
  }, async (req) => {
    // Reach the connected tool with a DECRYPTED secret — no crypto to re-implement:
    const conn = await ctx.resolveConnection(req.customerId, req.query.credentialId)
    // conn?.endpoint / username / password / apiToken / certificate — server-side ONLY, never sent to the client
    return ctx.db.$queryRawUnsafe(`SELECT * FROM splunk_indexes`)
  })
}

ctx.hasPermission(resource, action) returns a Fastify preHandler that enforces one of the app-scoped permissions your manifest declares under permissions.app. ctx.resolveConnection(customerId, credentialId) resolves one of the tenant's Connections to its decrypted ResolvedConnection (endpoint + secret) — tenant-scoped, server-side only, never returned to the client.

UI & Navigation Types

The SDK exports the app UI & navigation contract. The platform builds every app page's chrome — breadcrumb, header, navigation, permission gating, loading and error states — from these declarations; your component renders the page body. This is a live, validated contract — the CLI compiles your client/index.tsx into a hermetic bundle that renders inside the platform shell. See App UI & Navigation for the conceptual model.

import type { AppPageDeclaration, AppPageLayout, AppPageNav, AppPagePermission } from '@veltrixsecops/app-sdk'
import { APP_PAGE_LAYOUTS, APP_PAGE_NAV } from '@veltrixsecops/app-sdk'

/** How the platform frames the page body. */
type AppPageLayout = 'standard' | 'full-bleed' | 'canvas'
const APP_PAGE_LAYOUTS = ['standard', 'full-bleed', 'canvas'] as const

/** Where the page surfaces in navigation. */
type AppPageNav = 'sidebar' | 'tab' | 'hidden'
const APP_PAGE_NAV = ['sidebar', 'tab', 'hidden'] as const

/** An app-scoped permission (declared in permissions.app) required to see a page. */
interface AppPagePermission { resource: string; action: string }

interface AppPageDeclaration {
  path: string                       // route beneath the app, e.g. '/indexes' → '/apps/<app-id>/indexes'
  component: string                  // exported component name from the client entry
  label: string
  description?: string
  icon?: string                      // platform icon set; falls back to the app icon
  /** @deprecated use `nav` — kept for backward compatibility */
  sidebar?: boolean
  nav?: AppPageNav                   // defaults to 'sidebar' when sidebar:true, else 'hidden'
  parent?: string                    // required for nav:'tab'; must match another page's path
  group?: string                     // optional sidebar section label
  order?: number                     // deterministic ordering (ascending; ties break on label)
  layout?: AppPageLayout             // frame preset the platform renders around the body
  requiresPermission?: AppPagePermission  // hide page + nav entry unless the user holds it
}

Validated by CI

A requiresPermission must reference a permission your manifest declares under permissions.app — CI rejects a page that gates on an undeclared permission. nav: 'tab' requires a parent that resolves to another page's path.

Canonical Layout Helpers

The SDK exports the canonical app layout as runtime values, so tooling and generators agree with the platform's resolver.

import { APP_LAYOUT, HANDLER_NAMES, conventionalPaths } from '@veltrixsecops/app-sdk'

APP_LAYOUT.manifest        // 'manifest.yaml'
APP_LAYOUT.configTypesDir  // 'config-types'
APP_LAYOUT.canvasFile      // 'canvas.yaml'
APP_LAYOUT.serverEntry     // 'server/index'

HANDLER_NAMES
// ['validate', 'deploy', 'rollback', 'healthCheck', 'driftDetect', 'getStatus']

conventionalPaths('indexes')
// {
//   canvasTemplate: 'config-types/indexes/canvas.yaml',
//   defaultConfig:  'config-types/indexes/defaults.yaml',
//   handlers: { validate: 'config-types/indexes/validate', deploy: 'config-types/indexes/deploy', ... },
// }

define* Pipeline Helpers

Imported from @veltrixsecops/app-sdk/pipeline, these identity-style helpers give you full type inference on the context and return value without hand-annotating signatures. There is one per writable handler:

import {
  defineValidator,
  defineDeployer,
  defineRollbackHandler,
  defineHealthChecker,
  defineDriftDetector,
  defineConnectionTester,   // backs a Connection's "Test" button (manifest connectivity.testHandler)
} from '@veltrixsecops/app-sdk/pipeline'

export default defineDeployer(async (ctx) => {
  // ctx is inferred as DeployContext — no manual typing needed
  const { component, credential, canvas } = ctx
  return { success: true, message: `Deployed to ${component.hostname}` }
})

Both styles are valid: a plain export default async function deploy(ctx: DeployContext) with explicit types, or a define* wrapper for inference. getStatus is authored as a plain function typed with PipelineContext / ConfigStatus.

Next Steps