Developer Documentation
@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.
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.)
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)
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.
All five pipeline contexts extend a common PipelineContext. The deployment-oriented contexts add a target component plus credential and connectivity references.
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
}
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
}
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
}
Passed to healthCheck during and after a deployment.
interface HealthCheckContext extends PipelineContext {
component: ComponentRef
credential: CredentialRef | null
connectivity: ConnectivityRef | null
connectivityProvider: ConnectivityProviderRef | null
}
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
}
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
}
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
}
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.
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.
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`)
}
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.
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', ... },
// }
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.
@veltrixsecops/app-sdk/ui component kit.