Documentation

Integrate Flapjack with an external app platform

Auto-provision a Flapjack agent with a persistent Linux computer whenever a user creates an app in your platform.

If you run an app platform (your app, a project manager, an internal tool catalog) you can wire it to Flapjack so every app gets a dedicated agent with a persistent Linux computer. The agent runs tests, hosts dev servers, performs CI tasks — all scoped to that one app and shared across every conversation with the agent.

This guide walks through the your app v0 integration pattern.

Prerequisites

  • A Flapjack API key with sandbox:admin scope (create in Settings → API keys).
  • A webhook receiver on your platform (any public https URL).
  • Optional: a GitHub App installation if you want to clone private repos into the sandbox.

The flow

 your app UI                Flapjack
 ────────                 ─────────
 User creates app ──POST /api/agents/from-template──▶  create agent
                                                       enable persistent sandbox
                                                       store bootstrap config
                                                       kick off background bootstrap
                     ◀──── 201 { agent, bootstrapRunId } ───
                                                       bootstrap runs in Heyo VM
                     ◀──── POST webhookUrl (bootstrap.succeeded)
 your app marks app ready

 User clicks "Run tests" ──POST /api/agents/{id}/computer/exec──▶ exec in VM
                                               ◀── SSE stdout/stderr/exit ───

 App detail page           ── GET /api/agents/{id}/computer/status ──▶ cached pills
                                               ◀── JSON ───

Step 1 — Create the agent when the app is created

import { FlapjackClient } from '@maats/flapjack';

const flapjack = new FlapjackClient({ apiKey: process.env.FLAPJACK_API_KEY! });

export async function createApp(input: { name: string; repoUrl?: string }) {
  const app = await db.apps.insert({ name: input.name, /* ... */ });

  const result = await flapjack.createAgentFromTemplate({
    name: input.name,
    template: 'nextjs-fullstack',       // pick based on detected stack
    repo: input.repoUrl ? { url: input.repoUrl, installCmd: 'pnpm install' } : undefined,
    envVars: [{ key: 'NODE_ENV', value: 'development' }],
    sizeClass: 'medium',
    webhookUrl: `${process.env.PUBLIC_URL}/api/flapjack/webhook`,
    externalAppId: app.id,   // idempotency: safe to retry
  });

  await db.apps.update(app.id, {
    flapjackAgentId: result.agent.id,
    flapjackBootstrapRunId: result.bootstrapRunId,
  });

  return app;
}

The call returns in < 1 s — bootstrap (package install, repo clone, deps) runs in the background. Your UI can show "Provisioning…" and unlock the "Run" button on the webhook event.

Step 2 — Verify the webhook

Flapjack POSTs lifecycle events to your webhookUrl, signed with HMAC-SHA256 over the raw body.

// app/api/flapjack/webhook/route.ts
import { verifyWebhookSignature } from '@maats/flapjack';

export async function POST(req: Request) {
  const body = await req.text();
  const sig  = req.headers.get('x-flapjack-signature') ?? '';

  const ok = await verifyWebhookSignature(
    body, sig, process.env.FLAPJACK_WEBHOOK_SECRET!,
  );
  if (!ok) return new Response('invalid signature', { status: 401 });

  const event = JSON.parse(body);
  switch (event.event) {
    case 'bootstrap.succeeded':
      await db.apps.updateByDassieAppId(event.externalAppId, { status: 'ready' });
      break;
    case 'bootstrap.failed':
      await db.apps.updateByDassieAppId(event.externalAppId, { status: 'failed' });
      break;
    case 'computer.idled':
      // informational — next exec resumes automatically
      break;
    case 'agent.deleted':
      await db.apps.updateByDassieAppId(event.externalAppId, { flapjackAgentId: null });
      break;
  }
  return new Response('ok');
}

You get the signing secret from Flapjack org settings → Webhooks.

Step 3 — Run commands on demand

export async function runTests(appId: string) {
  const app = await db.apps.get(appId);
  if (!app.flapjackAgentId) throw new Error('agent not provisioned yet');

  const events = flapjack.execSandbox(app.flapjackAgentId, {
    command: 'pnpm test',
    timeoutSec: 300,
    workingDir: '/workspace/app',
  });

  let output = '';
  for await (const ev of events) {
    if (ev.type === 'stdout' || ev.type === 'stderr') output += ev.chunk;
    if (ev.type === 'exit') {
      await db.apps.recordTestRun(appId, { ok: ev.ok, output });
      return ev;
    }
  }
}

execSandbox is an async iterator. Each yielded event is one of exec_started / stdout / stderr / exit / error. Forward the chunks directly to your UI for a live terminal feel.

Step 4 — Show live status in the app page

import { useEffect, useState } from 'react';
import type { SandboxStatus } from '@maats/flapjack';

function useSandboxStatus(agentId: string) {
  const [s, setS] = useState<SandboxStatus | null>(null);
  useEffect(() => {
    let t: NodeJS.Timeout;
    const tick = async () => {
      const r = await fetch(`/api/flapjack/status/${agentId}`);
      if (r.ok) setS(await r.json());
      t = setTimeout(tick, 10_000);
    };
    tick();
    return () => clearTimeout(t);
  }, [agentId]);
  return s;
}

Or skip the custom UI entirely and embed Flapjack's status widget:

<iframe
  src={`https://app.flapjack.dev/embed/agents/${agentId}/status?token=${embedToken}`}
  style={{ border: 0, height: 180, width: '100%' }}
/>

Step 5 — Delete in concert

When a user deletes the your app app, delete the Flapjack agent to tear down the sandbox and stop the meter.

await flapjack.deleteAgent(app.flapjackAgentId);
await db.apps.delete(appId);

Pricing / idle

Persistent sandboxes auto-stop after 2 h of inactivity. The next exec call resumes them (status goes idle → waking → ready). You pay for running VMs only.

Rate limits

  • exec: 60/minute per agent, 600/minute per org, 3 concurrent per agent
  • status: excluded from exec quota (server-cached 10 s)

Failure modes

SymptomMeaningWhat to do
409 SANDBOX_NOT_READY on execBootstrap hasn't finishedWait for bootstrap.succeeded webhook
410 SANDBOX_DESTROYED on execVM gone (idle-stop + external destroy, or failed)Call /from-template again; it's idempotent
bootstrap.failed webhookPackage / repo install erroredRead stderr_tail from bootstrap_runs
429 RATE_LIMITEDOver quotaHonour Retry-After header
Docs last updated May 11, 2026