▞ 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