Safe Edge
Simulation Open the demo

One typed client, in the browser or on the API.

Read edges, flags and passes, and subscribe to events. The same calls run on the in-browser demo or against the v1 REST API.

Simulation The in-browser demo runs on labelled, simulated Elmina records.

Start in the browser, ship against the API.

@safe-edge/sdk has one method per operation in the contracts' operations table, so inputs and results are typed from the contract.

createSafeEdgeClient takes a transport. memory runs the Safe Edge core in the page on the labelled Elmina seed; http calls the v1 API. A screen written against the shared SafeEdgeClient type runs on either.

TypeScript, the memory transport
import { createSafeEdgeClient, elminaSeed } from '@safe-edge/sdk'

const demo = createSafeEdgeClient({ transport: 'memory', userId: 'usr_manager' })
const segments = await demo.listSegments()
const s1 = await demo.getSegment({ params: { id: elminaSeed.story.s1.segmentId } })
const open = await demo.listFindings({ query: { status: 'open' } })
TypeScript, the http transport
import { createSafeEdgeClient } from '@safe-edge/sdk'

const token = '<your bearer token>'
const api = createSafeEdgeClient({ transport: 'http', baseUrl: 'http://localhost:3000/v1', token })
const expiring = await api.listPasses({ query: { state: 'expiring', expiresWithinDays: 30 } })
createSafeEdgeClient options
Transport Option What it is
memory userId The seeded user the client acts as, such as usr_manager; switch with client.as(userId)
planner, optional Needed by createPatrolPlan and by re-planning; build it with createElminaPlanner
clock, optional The scenario clock; the wall clock when absent. The simulated drone flies on it.
http baseUrl The API's /v1 base, such as http://localhost:3000/v1
token The bearer token; the API maps it to a user and a role
fetch, optional The fetch to call the API with; the global fetch when absent

Every seeded record carries simulated: true and source: 'fixture'. Show the label on screen.

A v1 REST API, described in OpenAPI 3.1.

JSON under /v1, generated from the Zod contracts in packages/contracts: 65 operations, each with the roles allowed to call it.

  • Bearer tokens carry a role. Demo tokens work only in demo mode; the API rejects them.
  • Every write accepts an Idempotency-Key header. A retry with the key within 24 hours replays the first answer. The SDK sends a fresh key on every write; pass your own to retry safely.
  • A refused action returns 403 with the policy rule that blocked it.
  • Responses are checked against the contract on the client too.
A selection of v1 operations, with the roles allowed and the scope an agent needs
Method and path Roles Agent scope
GET /assets, GET /segments/{id} any edge.read
POST /patrol-plans manager, system patrol.propose
POST /patrol-plans/{id}/approve pilot refused
GET /candidates manager inspection.read
POST /candidates/{id}/decision manager refused
POST /work-orders manager workorder.draft
POST /work-orders/{id}/submit manager, engineer refused
POST /retests reviewer refused
GET /passes any edge.read
GET /events any edge.read
POST /webhooks integrator refused

In the OpenAPI document, x-safe-edge-roles lists the roles allowed and x-safe-edge-agent-scope names the scope an agent token needs; when it is absent, agents are refused.

TypeScript, a refusal
import { SafeEdgeDeniedError } from '@safe-edge/sdk'

try {
  await demo.as('usr_agent').submitWorkOrder({
    params: { id: 'wo_1' },
    body: { installerUserId: 'usr_installer', dueAt: '2026-10-21T08:00:00.000Z' }
  })
} catch (error) {
  if (error instanceof SafeEdgeDeniedError) console.log(error.rule) // 'agents_draft_only'
  else throw error
}

Roles for people, scopes for the agent.

The server checks the role, or the agent's scope, on every request.

Roles and agent scopes
Kind Values
Roles manager, engineer, reviewer, installer, pilot, gm, crew, integrator, agent, system, provider
Agent scopes edge.read, inspection.read, finding.propose, workorder.draft, notice.draft, patrol.propose
Never issued edge.pass.accept, repair.verify, workorder.submit, notice.send: absent from the contract, so no token can hold them

Policy lives in the core, so the server and the in-browser demo enforce the same rules. The person who records a repair cannot accept it, and only an accepted re-test by a reviewer makes a pass valid. See what the agent can and cannot do.

Thirteen events, as CloudEvents 1.0.

Every state change the loop cares about is an event with source safe-edge://townships/{id} and subject segments/{id}.

Payloads carry IDs and simulated, never images. Consumers fetch records and evidence through the API. The AsyncAPI document is in packages/contracts.

Event types
Type When
edge.patrol.planned.v1 The planner built a patrol plan; a pilot must approve it before it flies.
edge.patrol.started.v1 A pilot started a patrol from an approved plan.
edge.patrol.completed.v1 A patrol landed and reported coverage per segment.
edge.candidate.raised.v1 A provider, the system or the agent raised a candidate; the core assigned its tier.
edge.candidate.decided.v1 A manager confirmed, rejected or asked to recapture a candidate.
edge.pass.suspended.v1 A segment's pass was suspended by a Tier 2 candidate: a missing or changed safety item, a damage report or a missed patrol.
edge.work_order.drafted.v1 A person or the agent drafted a work order.
edge.work_order.submitted.v1 A manager or engineer submitted a work order with an approved remedy.
edge.repair.completed.v1 The installer marked the repair complete.
edge.retest.recorded.v1 A reviewer recorded a re-test of a repair they did not install.
edge.pass.issued.v1 An accepted re-test issued a pass valid for at most 12 months.
edge.pass.expiring.v1 A pass entered its last 30 days.
edge.pass.expired.v1 A pass reached its end date.

Signed webhooks, or a live stream.

An integrator registers a webhook for the event types it needs. A screen can read the same events as server-sent events from GET /v1/events.

  • Deliveries are signed per Standard Webhooks: every one carries webhook-id, webhook-timestamp and webhook-signature, a base64 HMAC-SHA256 of the ID, the timestamp and the raw body.
  • The signing secret (whsec_…) appears only in the registration's response.
  • A webhook's origin must be on the API's allowlist, SAFE_EDGE_WEBHOOK_ORIGINS, checked at registration and again before every delivery.
  • Network errors, timeouts, 408, 429 and 5xx are retried; each webhook keeps its latest 20 failures.
  • The stream has no replay: events from before it opened are not sent.
TypeScript, the http client
const hook = await api.registerWebhook({
  body: { url: 'https://example.com/safe-edge', eventTypes: ['edge.pass.suspended.v1', 'edge.pass.issued.v1'] }
})
// hook.secret signs every delivery; this response is the only place it appears

const events = await api.streamEvents() // subscribed once this resolves
for await (const event of events) console.log(event.type, event.subject)

No vendor lock-in for drones or work orders.

Provider adapters keep the core independent of any drone vendor or work-order system.

Provider adapters and their status
Adapter Status Maps to
drone-sim Working The scene's flight model and scenario events. It waits in flight at unreleased separation holds and follows approved re-plans.
drone-dji-cloud Typed stub DJI Cloud API: dock binding, wayline tasks, live stream start and the media upload callback
drone-mavlink Typed stub MAVLink mission upload through a small server-side bridge
workorder-procore Sandbox Procore Observations. API-created observations do not notify assignees automatically.

An MCP server for AI assistants.

Claude Code or Claude Desktop can read, propose and draft through the MCP server, with the same narrow scopes as the built-in agent.

The server runs over stdio and calls the v1 API with the agent's token, so the API enforces every scope and state rule. It reads the API's address and the token from your environment; the token has no default.

apps/mcp/mcp.json
{
  "mcpServers": {
    "safe-edge": {
      "type": "stdio",
      "command": "bun",
      "args": ["apps/mcp/src/index.ts"],
      "env": {
        "SAFE_EDGE_API_URL": "${SAFE_EDGE_API_URL:-http://localhost:3000/v1}",
        "SAFE_EDGE_AGENT_TOKEN": "${SAFE_EDGE_AGENT_TOKEN}"
      }
    }
  }
}

The eight tools, and what none of them can do, are on the agent page.

The demo runs on this client.

The in-browser demo uses the memory transport, on the same core and the same rules as the API.