Skip to content

Stock and invoices that stay right when the software around them gets it wrong.

Alder & Ember Roastery sells through Shopify, Stripe and Square and keeps its books in Xero. Those services resend, drop out and lose access: pick a failure below and watch Greenlot handle it, live.

prrr

Order a coffee, pull the lamp, pet the cat.

Starting Greenlot in your browser. Setting up its database takes a few seconds.

  • Applied
  • Repeat, ignored
  • Failed, will retry
  • Held for a person

Greenlot is starting up in this tab

  1. Loading Greenlot’s code
  2. Starting PostgreSQL in this tab
  3. Applying its database migrations
  4. Opening the roaster’s shop
  5. Replaying this morning’s orders on the map

Everything runs inside this page. Nothing is installed, and nothing leaves your browser.

  • Applied
  • Repeat, ignored
  • Failed, will retry
  • Held for a person

Select any box on the map, or a moving dot, to see what Greenlot stored for it.

Pick something to go wrong

Greenlot handles it on the map above, and the one record that proves it appears here.

A copy, struck through in the ledger
A retry, counting down to its next try
A queue, filling and draining

Each button sends real, signed messages into your own copy of the demo.

prrr

Order a coffee, pull the lamp, pet the cat.

The life of one order

Appears once your copy of the demo has applied an order: one real order, followed from the shop to the ledger.

Rewind to any message

Greenlot keeps every message exactly as it arrived and never edits one. So it can set its stock aside and work it out again from those messages alone. Move back to see the stock as it stood after any of them.

Under the hood

Rewind reads raw_events, which PostgreSQL won’t let anyone update or delete, and replays each message the worker has finished through the worker’s own code: the provider’s parser, the duplicate check on event keys and applyIntegrationEvent, in the order they arrived, on an empty copy of the workspace. Nothing is written. Ledger rows no message caused (the opening stock, a person’s transfer or correction) are taken as recorded, at their time. Then the figures it reaches are compared, item by item, with the stock_movements table. GET /api/tenants/:id/rewind

Appears once your copy of the demo is running.

Every message, with its record

The same events as the map, newest first. Open Under the hood on any line to see what Greenlot stored: the signature check, the event key, every attempt and what it changed.

Nothing yet. Press one of the buttons and the events will appear here as they happen.

Connected to real accounts

The connections are real: Greenlot signed in to a test account at each of these providers, read real data back, and created one real invoice.

Shopify development store ·

Signed in to a real Shopify development store and read from its admin API.

Under the hood
Request
POST /admin/api/graphql.json → 200
API
2026-07
Access
OAuth / server credentialsread_ordersread_productsread_inventoryread_locations
Records
1 in the reply
Saved reply
{
  "data": {
    "shop": {
      "plan": {
        "partnerDevelopment": true
      }
    }
  }
}

Trimmed to fields that can’t identify anyone.

Rate limit
1998 of 2000 query points left, refilling at 100/s

Square sandbox ·

Signed in to a Square sandbox account and read its shop location.

Under the hood
Request
GET /v2/locations/{redacted} → 200
API
2026-09-16
Access
personal sandbox token; full sandbox permissions, reads only in Greenlot
Records
1 in the reply
Saved reply
{
  "location": {
    "country": "US",
    "currency": "USD",
    "status": "ACTIVE"
  }
}

Trimmed to fields that can’t identify anyone.

Xero demo company ·

Signed in to Xero’s demo company and read its contact list.

Under the hood
Request
GET /api.xro/2.0/Contacts → 200
API
Accounting API 2.0
Access
OAuth / server credentialsaccounting.contacts.readaccounting.settings.readoffline_access
Records
50 in the reply, 3 shown
Saved reply
{
  "Contacts": [
    {
      "ContactStatus": "ACTIVE",
      "IsCustomer": false,
      "IsSupplier": false
    },
    {
      "ContactStatus": "ACTIVE",
      "IsCustomer": false,
      "IsSupplier": false
    },
    {
      "ContactStatus": "ACTIVE",
      "IsCustomer": false,
      "IsSupplier": false
    }
  ]
}

Trimmed to fields that can’t identify anyone.

Rate limit
59 calls left this minute, 999 today

QuickBooks sandbox ·

Signed in to a QuickBooks UK sandbox, read the company’s details and created one invoice.

£24.00

The invoice, VAT included. Sent once, with a fixed request ID, then read back: customer, lines, tax and total matched.

Under the hood
Request
GET /v3/company/{redacted}/companyinfo/{redacted} → 200
API
Accounting API v3 · minor 75
Access
OAuth / server credentialscom.intuit.quickbooks.accounting
Records
1 in the reply
Saved reply
{
  "CompanyInfo": {
    "Country": "GB"
  }
}

Trimmed to fields that can’t identify anyone.

These are saved, redacted copies of real responses, with account IDs removed. Nothing on this page can sign in to or change those accounts.

How it’s built

Five decisions do most of the work. Pick one to see the reason in a sentence and a drawing, then the code that does it, then watch it happen on the live map.

Whatever happens later, there is always an exact copy of what the provider sent, and nobody can quietly edit it.

Under the hood

Intake checks the HMAC signature against the raw bytes, stores the payload and headers in raw_events, acknowledges, and queues a job. A database trigger makes raw_events append-only. Messages with a bad signature are kept for audit but never queued.

-- Stored provider payloads are evidence: no edits, no deletes.
-- A row goes only with its tenant (ON DELETE CASCADE), and the tenant
-- is looked up in the table's own schema, never the caller's path.
CREATE OR REPLACE FUNCTION guard_append_only() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  tenant_exists boolean;
BEGIN
  EXECUTE format(
    'SELECT EXISTS (SELECT 1 FROM %I.tenants WHERE id = $1)',
    TG_TABLE_SCHEMA
  ) INTO tenant_exists USING OLD.tenant_id;

  IF tenant_exists THEN
    RAISE EXCEPTION 'Audit records are append-only';
  END IF;
  RETURN OLD;
END;
$$;

CREATE TRIGGER raw_events_immutable
  BEFORE UPDATE OR DELETE ON raw_events
  FOR EACH ROW EXECUTE FUNCTION guard_append_only();

Simplified from the source for reading; behavior unchanged.

435

automated tests, all passing

54 files on the last full run, November 3, 2026. The run writes these figures itself, so they can’t drift from the code.

Every failure in the demo above is one of them.

So are checks that one business can never read another’s data, and that an invoice is posted exactly once even when two requests race. They run against PostgreSQL.

Five of the 54 files, as they are in the repository

TypeScript · Fastify · PostgreSQL · Drizzle · pg-boss · Next.js · Zod · AES-256-GCM for stored credentials

What we’d still do before running this in production
  • Turn on live sync. Real accounts are connected and read today, but the worker only processes demo traffic.
  • Add PostgreSQL row-level security as a second layer behind the data layer.
  • Split the per-business state document into normalized tables before it grows.
  • Share rate limits across servers, and set retention rules for stored messages.