← Purpul Hive

What is this?

This is an API you can point existing tools at. It stores your data, answers questions about it over plain URLs, and can suggest what to do next. This page is everything you need to build against it.

Words used on this page

Six terms come up constantly. They are defined here so nothing below assumes you already know them.

TermWhat it means
the boardthe engine that stores your data and answers questions about it. You never talk to it directly.
the gatethe HTTP server in front of the board — the thing you actually call. Everything on this page is the gate.
a worldone dataset with its own rules. A shop, a clinic, a school timetable. Your data lives in a world.
a lensa single HTML file that displays a world. Changing the lens changes what you see, never the data underneath.
an adaptera translator that makes the gate speak a protocol you already have a client for — S3, PostgREST, Meilisearch, Redis. You keep your existing client and change the URL.
a signalone plain fact about right now, as a short string: cheese_in_basket, friday. You send signals; you get back suggested actions.

The contract

Every route this server offers is listed in an OpenAPI document that the server generates about itself. It cannot go out of date, because it is not written by hand. Start here:

curl -s $GATE/openapi.json | jq '.paths | keys'

If a path appears there, it works. You can generate a client from that file with the usual tools. Its servers entry is the public address you should call, which on a hosted box is not the port the process is listening on internally — so use what the document says, not what the logs say.

RouteWhat it does
POST /api/observesend signals, get suggested actions back as a stream
/api/{collection}your data: list, create, read, update, replace, delete
/api/verb · /api/witnessrun an action, and read the tamper-evident log of every action run
/v1/chat/completionsthe same suggestions through an OpenAI-compatible endpoint, so an existing chat client works
/s3/{bucket}/{key}file storage that speaks S3 — use any S3 client
/lenses/… · /x/…published lenses, and any adapters this box declares
/healthwhat this box has switched on, and what is sealed

Deleting hides a record; it never destroys it. A delete writes a “this is gone now” marker instead of removing the row, so the old values are still there. That is why you can always ask what a record said last week — see /remember below.

The loop

This is the part that suggests what to do next. You send a few short facts about the current situation; you get back suggested actions. The reply is a stream (text/event-stream), so results arrive as they are worked out rather than all at the end.

In the example below: someone has cheese in their basket, it is Friday evening, and your page has one slot named hero you are willing to fill.

curl -N -X POST $GATE/api/observe \
  -H 'content-type: application/json' \
  -d '{"signals":["cheese_in_basket","friday","evening_slot"],"slots":["hero"],"hour":19}'

Five events arrive, always in this order:

  1. address — an id for this exact combination of signals. The same signals always give the same address.
  2. peaks — the six situations that best match what you sent, strongest first.
  3. pairings — items that tend to go together here, worked out from the data rather than configured.
  4. either directives (what to do) or probe (it does not know, and here is its best guess) — never both.
  5. done — the stream ends here.

The mistake almost everyone makes: writing a client that only listens for directives. You will get probe instead whenever the system does not recognise the situation, and your page will silently show nothing. Handle both.

Why there are two: it only gives you a confident answer when it recognises the whole situation. If a known pattern needs four signals and you sent three, it will not fill in the fourth and pretend. It sends a probe instead — a labelled guess you may accept or ignore. Being told “I am not sure, but possibly this” is more useful than a confident wrong answer.

Two smaller rules:

The grammar

You query by building a URL. There is no query language to learn and no query parameters — filters, sorting and paging are path segments, read left to right in the order you wrote them. Any query is therefore a link you can bookmark, cache or send to someone.

About sixty of these words exist. The complete list lives next to the code at packages/baas-data/GRAMMAR.md.

/api/orders/where/status/eq/paid/order-by/total/desc/limit/10
/api/orders/include/customer/match/customer_id
/api/orders/count
/api/products/tags
/api/products/neighbours/ceramics,handmade
GroupWords
Recall and naming/identifier /hash /version /remember /exists /meta /seed
The select/where /or /and /order-by /skip /limit
Arithmetic/count /any /sum /avg
Shape/tags /neighbours /include…/match /union /i18 /convert
Wave and walk/superpose /merge /next /previous

Four rules decide how a URL is read. They matter when a query does something you did not expect:

  1. Half a phrase does nothing. /include without a following /match is ignored rather than guessed at.
  2. Your values are never mistaken for keywords. In /where/status/eq/count, count is the value you are filtering for, not the count operator.
  3. Some words end the URL. Once you ask for /count you have a number, and nothing can follow it.
  4. The data decides ambiguous cases. A join works out its own direction from which side holds the linking field; a comparison sorts numerically when both sides are numbers and alphabetically when they are not.

/remember returns every previous version of a record, oldest first — the audit trail, free, because updates add versions rather than overwrite.

Adapters

An adapter makes this server answer as though it were a service you already use, so you keep your existing client library and change only the URL and the key. An adapter with no key configured is sealed: it refuses everything and says so, rather than half working. /health lists which are open on any box:

curl -s $GATE/health
AdapterSpeaksConfigure with
storageS3, SigV4S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY
supabasePostgRESTSUPABASE_KEYS, SUPABASE_SERVICE_KEYS
meilisearchMeilisearchMEILI_KEYS, MEILI_ADMIN_KEYS
redisRESPREDIS_PORT, REDIS_PASSWORD
panela judgement portPANEL_URL, PANEL_KEY, PANEL_TOKENS
identity / oauth / entrasign-inAUTH_JWT_SECRET, OAUTH_*, ENTRA_*

Each also takes an optional *_PREFIX if you want it on a different path. Point your existing S3 or PostgREST client at the gate with the matching key and it should work unchanged.

The panel is optional. It forwards a question to several language models, then asks one more to weigh their answers and decide. You supply the models: PANEL_URL is where they live, PANEL_DELIBERATORS names the ones that answer, PANEL_CHAIR the one that decides. Without PANEL_URL it stays sealed.

Registering your own adapter

You do not need this repository. An adapter is four facts, so it is four environment variables — declare one and the gate mounts it on the next deploy.

CONNECTOR_SHEETS_URL=https://api.example.com/v4     # declares it. Required.
CONNECTOR_SHEETS_KEYS=k1,k2                        # who may call. Default: GATE_TOKENS
CONNECTOR_SHEETS_UPSTREAM_KEY=…                    # injected. Never the caller's.
CONNECTOR_SHEETS_PREFIX=/x/sheets                  # default /x/<name>
CONNECTOR_SHEETS_HEADER=x-api-key                  # optional: header instead of Bearer

Then GET /x lists every declaration — including the ones that did not load, and why. A connector that silently failed to load is exactly what that list exists to prevent.

Two credentials that never meet. You prove yourself to the gate with a key the gate knows. The gate proves itself to your upstream with a credential you never see. Your Authorization header is dropped, not forwarded.

Private upstreams are refused by default. Loopback, link-local, RFC1918 and *.internal addresses do not load — a gate that would proxy to 169.254.169.254 is a credential leak with a REST interface. If you mean it, opt that connector in by name with CONNECTOR_<NAME>_ALLOW_PRIVATE=1.

Lens worlds

A lens is one HTML file that displays a world. Anyone can read every published lens; the two shelves differ only in who is allowed to put something on them.

ShelfWho can publishWho can read
verifiedthe owner of this system only, using a cryptographic signatureanyone
communityanyone holding a keyanyone
curl -s $GATE/lenses/index.json

curl -X PUT $GATE/lenses/community/my-lens \
  -H "authorization: Bearer $KEY" \
  --data-binary @my-lens.html

Every entry in the listing says which shelf it came from, so a page cannot show a community lens with a verified badge. No key of any kind can publish to the verified shelf — it takes a signature from a private key that is not issued to anyone.

Keys

KeyOpensHow you get it
an invite key
the one you probably want
building worlds, the lens shelf, /v1/*, publishing a community lensapply, or paste one you were given into Gain access
an adapter keyone adapter only — the S3 key opens S3 and nothing elseset by whoever runs the box
a short-lived granta single write, for a few minutesissued automatically; you rarely handle one
the owner's signaturethe verified shelf, the operator toolsnot issued to anyone. It is a private key held on one machine.

Send an invite key as a bearer token:

curl -s $GATE/v1/models -H "authorization: Bearer $KEY"

A box with no keys configured is sealed: you can read, every write returns 401, and /health says so plainly. That is a deliberate setting, not a broken box — a public demo is meant to be sealed.

The mimic

The mimic is a testing tool. It records the real API calls your app already makes, then replays them — so you can point the app at this gate instead of its real backend and check nothing breaks. If your app cannot tell the difference, the replacement is faithful.

mimic --verify           # replay every captured call, byte for byte
mimic --listen 8900      # serve it, so a client can be pointed here

--verify replays every recorded call and compares the response byte for byte. Any difference fails. Endpoints that generate something new each time are excluded from that comparison, because replaying a recorded suggestion would only ever repeat an old one.

With gidget: the extension watches a real app, records what it asks for, and the mimic serves that back. Download it top right, unzip it, and load it unpacked in your browser’s extension page. The button only appears on a box that carries the navigator.

The mimic is a command you run yourself, not an endpoint. It is included in the container, but no URL exposes it.


Everything above can be checked against the running server. Start with $GATE/openapi.json and $GATE/health — and if they disagree with this page, the server is right and this page is out of date.