▞ koshmana
AN EVENT-SOURCED DATA SERVICE ON ACTIVITY STREAMS
The append-only log is the sole source of truth. Every table, every view, every permission is a projection folded from it — wipe them all and replay brings every one back, exactly. Activities in, worlds out.
WORLDS, NOT SILOS
Each project is its own log with its own consumer, catalog, and permissions — one deployment, many worlds, each at /<name>.
AUTH OBEYS THE LOG
Grants are events. Minting is a Create, revocation a Delete, and replay rebuilds the whole authority set. Nothing hides.
CAUSE BECOMES EFFECT
The playground shows the machine: every sequence number, every projection, live. It looks like a toy until it looks obvious.
How to use Koshmana — quickstart for agents & humans
Koshmana is a backendless, multi-tenant hypermedia platform on Activity Streams 2.0. You append activities (AS2 envelopes) to an append-only log — the log is the only source of truth; every table, page, permission, and identity is a rebuildable projection folded from it. Every object is a dereferenceable URL, reads can be public, and the same URL serves AS2 JSON to a script or a rendered HTML page to a browser.
The log is truth · state is a projection · fix data by appending or tombstoning, never by editing.
1 · Start — a token is the whole config
Set just KOSHMANA_TOKEN. The URL defaults to
https://koshmana.com. The actor comes from the token — a
per-agent grant binds one, so writes omit actor and the
server stamps it; only an actor-free token (root/dev) needs
KOSHMANA_ACTOR. Writes take
Authorization: Bearer <token>; public reads take none.
pip install koshmana # CLI + Python client (add "[mcp]" for the MCP server)
export KOSHMANA_TOKEN=<token> # the whole config
# KOSHMANA_URL=http://127.0.0.1:8600 # only for a local playground
# KOSHMANA_ACTOR=…/actors/<you> # only for actor-free (root) tokens
2 · The CLI — the whole surface
koshmana collections # what collections exist here
koshmana describe issues # a collection's full declaration
koshmana bindings journal # materialized bindings (parameters/slices)
# READ a projection (default entry "recent")
koshmana get issues
koshmana get issues --slice by-status open # a board column (a slice)
koshmana get issues --where status=open --where priority=high # ad-hoc filter (ANDed)
koshmana get issues --search '"exact phrase" -noise' # full-text fallback
koshmana get issues --order priority --descending # ad-hoc sort
koshmana get issues https://koshmana.com/koshmana/issues/42 # find ONE object by id
# WRITE — the append IS the commit (actor filled from the token)
echo 'content: hi' | koshmana append notes -f - # bare object → wrapped Create
koshmana append issues -f envelope.yaml # carries actor/object → sent as-is
koshmana edit issues <id> --field status --find open --replace closed # anchored find/replace
koshmana upload posts ./logo.png --type Image # blob → R2 (content-addressed) + object
koshmana declare -f decl.json # new collection via the catalog
# LIVE · LOG · TOKENS
koshmana tail issues # snapshot, then the live wire, forever
koshmana subscribe issues --webhook https://ex.io/hook # durable at-least-once delivery
koshmana log --from 0 # the raw log (truth)
koshmana invalidate notes 12 --reason "…" # tombstone a bad event + compensate
koshmana token mint --name ci --actor …/actors/ci \
--collections notes,issues --ops read,write # mint a scoped agent token
koshmana token list / revoke ci # audit / revoke
edit changes text inside one field with no read
round-trip (a koshmana:Edit, server-side anchored
find/replace); a plain
Update merges — send only the fields you are
changing (omitted fields survive, null deletes one);
Delete tombstones it. Output is JSON (the wire); add
--yaml for the human view. Address another tenant with
--url https://koshmana.com/p/<project>.
3 · Python & raw HTTP
from koshmana import Koshmana
k = Koshmana(token="<token>") # url defaults to https://koshmana.com
ack = k.append("notes", {"type":"Create","object":{"type":"Note","content":"hi"}})
obj = k.get("notes", ack["object"]) # find one object by id (None if absent)
k.update("issues", obj["id"], status="closed") # one delta append: the fold merges, the rest survives
page = k.query("issues", "recent", where={"status":"open"})
for act in k.tail("issues"): ... # snapshot, then live activities, forever
# append — this IS the commit (actor optional: the token fills it)
curl -X POST https://koshmana.com/collections/notes/events -H "Authorization: Bearer $TOK" \
-H "Content-Type: application/json" -d '{"type":"Create","object":{"type":"Note","content":"hi"}}'
# dereference an object by its URL id — content-negotiated:
curl -H "Accept: application/json" https://koshmana.com/<project>/<coll>/<key> # AS2 JSON
curl -H "Accept: text/html" https://koshmana.com/<project>/<coll>/<key> # rendered page
4 · Projects — your own tenant
Every project is its own log, catalog, and authority, addressed at
/p/<project>. Creation is self-service: any
verified identity creates one and becomes its admin — no root in the
path. As admin you declare collections and mint agent tokens for it.
# any verified Google identity → a project it administers
curl -X POST https://koshmana.com/projects -H "Authorization: Bearer $ID_TOKEN" \
-d '{"name":"atlas","title":"My project"}'
koshmana --url https://koshmana.com/p/atlas collections # then work inside it
5 · Access control — the resource decides
The token proves who you are; the collection or slice declares who may act; effective permission is the intersection. A declaration can carry:
audience: [ "as:Public" ] # who may READ — an actor id, a Group, or as:Public (tokenless)
koshmana:writers: [ "…/actors/alice", "…/groups/editors" ] # who may WRITE
attributedTo: "…/actors/owner" # the owners (manage the resource)
koshmana:Owner on a slice gives each user their own
rows only (a per-user data space, keyed server-side to their actor).
Group membership is data: Add / Remove an actor
to a Group object. Undeclared collections stay project-scoped as
before.
6 · Identity — first-class actors
An actor is a real, public object (Person / Service /
Application) with a stable id and a mutable handle. A Person
is created on Google sign-in, a Service when you mint its token.
Dereference anyone by handle — no PII, no token:
curl https://koshmana.com/@alice # → the public actor profile
# claim a friendly handle (get-then-update; nothing else moves):
k.update("actors", actor_id, preferredUsername="alice")
7 · Media, pages & sites
A collection's declaration turns data into the web:
public: true # tokenless, embeddable reads (CORS-open)
template: "<h1>{{title}}</h1>…" # render ONE object as an HTML page (browsers get it, scripts get JSON)
listTemplate: "…" # render a read model / index as an HTML list page
site: true # serve files by path — host a whole static website
koshmana upload posts ./logo.png --type Image # content-addressed blob on R2
curl https://koshmana.com/koshmana/www/ # a site → index.html by path
curl https://koshmana.com/cdn-cgi/image/width=400,format=auto/<blob-url> # edge resize + webp/avif
8 · Live & delivery
koshmana tail <coll> (or k.tail(…),
or SSE at /collections/<coll>/live) streams a snapshot
then live activities forever. koshmana subscribe … --webhook
gives durable, at-least-once webhook delivery you can
unsubscribe later.
Full reference: the client README,
docs/cli.md, and the per-topic docs — auth, projects,
access, actors, templates, hosting, blobs. Shell-less hosts use
koshmana-mcp (same client underneath).
built on S2 · state in Postgres, truth in the log