Ignitify.dev

Multiple tenants & hosts.
No shared blast radius.


Read the tier modelSee how isolation holdsjailed hypervisor · separate kernel · de-privileged child

Three boundaries, each verified separately.

Tenants don’t share a kernel, a uid, or a network path. Each boundary was tested on its own, because a boundary you haven’t tested is a boundary you’re assuming.

The jailed hypervisor

bounds what a hypervisor escape reaches

A per-VM uid, never shared between two live VMs, chrooted to that VM’s files only. Enabled by default in every environment — the production launch path is the only launch path.

The MicroVM

bounds the guest’s reachable network

Separate kernel, its own drives (code, scratch, and an optional packages drive), no shared filesystem. Cross-tenant and guest→host traffic are both dropped on two independent paths.

The de-privileged child

bounds what the guest can do

Your code runs as uid 10001 with --no-new-privs. Unconditional — there is no configuration that turns it off.

Concepts

Four levels, each owning exactly one thing.

tenantidentity
An account, and the unit of isolation and auth. Owns hashed API keys, the granted tier, the secrets vault, the monthly egress rollup — and no policy of its own. Everything a tenant may do comes from its tier, and nowhere else.
taska name
A reusable definition, unique per tenant by slug. Slugs are burn-forever: permanently reserved even after deletion, so a deleted name can never be silently reused by something new.
deploymentan image
One immutable, content-addressed build. Owns the resolved execution nature and the bootable code image. Identical content deduplicates instead of rebuilding.
runone job
One execution of one deployment, in one MicroVM. Owns the payload, an immutable instruction snapshot, the boot fingerprint, its slot and IP, and its own per-run callback token and secret.

One run, end to end.

What happens between task:run and a result. Four participants, and the checks that can refuse it — each answering a different question.

  1. 1
    task:run

    Sent with your API key and a payload. A load balancer hands it to either of two brains. The brain answers accepted at once; the VM starts a moment later, and any brain can start it.

  2. 2
    Is this kind of work allowed?

    Capability and size-ceiling check. 403 here is the real authority; 400 already caught it at deploy, before any VM.

  3. 3
    Within the monthly allowance?

    Compute and egress for the month. Refuses with 429. It clears when the month resets, or on a bigger tier.

  4. 4
    Admit · record

    Concurrency and rate limits (429 if over), the run row and the month’s counter — all in one transaction under a per-tenant lock, so two brains racing the same tenant cannot both win. If the fleet is full, an ordinary run waits in the queue for a limited time, then fails; a run sent with on_busy: reject fails at once. A run that asks for a pinned VM is refused at once: 429 at your plan’s limit on pinned VMs, 503 when no host has a pinned slot left.

  5. 5
    Spawn

    The brain orders a VM from one host, over mutual TLS. If a warm VM is ready, steps 5 to 7 are skipped.

  6. 6
    Boot

    A jailed Firecracker process under its own uid. Separate kernel, read-only code image, read-write scratch.

  7. 7
    Fence the VM

    The brain sends the host a signed network policy. A Rust daemon makes the privileged changes: it adds the VM’s address to the kernel’s firewall sets, sets its bandwidth, and updates the egress proxy’s allowlist. Your code cannot change any of it. A host that cannot enforce the policy fails the run.

  8. 8
    Pull payload · push result

    The guest opens every exchange, over a private channel to the host — never the network. The payload comes back from the brain through the host. Your function runs, then progress and the result go back the same way. A result is capped at 10 KiB.

  9. 9
    Settle · teardown

    The brain stores the result, then the run is done. The VM is destroyed or kept warm for the next run. Its run time and network bytes are billed.

  10. 10
    run:get

    Status, result, and the last streamed chunk — from whichever brain answers, not the one that ran it. The CLI polls it.

Two ways in for a webhook.

An outside system can reach Ignitify in two different ways.
One starts a run. The other reaches a run.

Trigger

An event starts a run.

POST /hooks/trigger/<hook-id>

  1. 404Unknown hook.
  2. 413Body over 10 KiB.
  3. 401Bad signature: x-ignitify-signature, HMAC-SHA256 of the raw body.
  4. 429Too many deliveries, or a plan limit reached.
  5. 202A run is created, with the body as its payload.
  • One delivery, one run. A retry with the same delivery id gets the same 202 and no second run.
  • Set up once. hook:create prints the URL and the signing secret. The secret is shown one time.
  • Safe by default. A triggered run has no network, and it cannot start a task that listens for inbound traffic.
Callback

A reply reaches a running task.

https://<route>--<tenant>.<domain>/__callback/<token>

  1. 1Every run gets its own address and key: ctx.callback.url.
  2. 2Your task gives the URL to a provider that will answer later.
  3. 3The edge sends the request to the one VM of that run. It never reads the body.
  4. 4Your code calls ctx.callback.verify(raw, signature). The key stays outside your code.
  • Only for bidirectional tasks. Any other run has no route and gets 404.
  • It ends with the run. An unknown or finished token gets the same 404.
  • Own scheme? A provider that signs its own way can still use the URL. Your task checks the token the provider sends.

Ignitify runs on bare metal.

Firecracker needs direct access to the CPU's virtualisation — it opens /dev/kvm on the host — so there is no other tenant's hypervisor underneath yours. The boundary around your job is the machine's own, not a scheduler's promise.