- Python 96.6%
- Makefile 2.6%
- Dockerfile 0.4%
- Shell 0.4%
The MCP streamable-HTTP transport returned empty responses through the backend's two BaseHTTPMiddleware middlewares - a long-open stream does not survive that stack, which is why mcporter timed out at 30s. The MCP server now mounts on its own bare FastAPI carrier with no middleware, and a pure-ASGI passthrough middleware diverts /mcp to the carrier before the BaseHTTPMiddleware stack runs. Same pattern eco-mcp-app uses: keep streamed MCP transport clear of BaseHTTPMiddleware. closes #87 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Audit-log: coily://1779452888/AGPE7KCE - coily git add backend/main.py Audit-log: coily://1779452891/AGPE7KCQ - coily ops aws sts get-caller-identity Audit-log: coily://1779452891/AGPE7KCT - coily ops aws ssm get-parameter Audit-log: coily://1779452892/AGPE7KCT - coily git commit Audit-log: coily://1779452897/AGPE7KDJ - coily git push Audit-log: coily://1779453303/AGPE7LU3 - coily ops aws ssm get-parameter Audit-log: coily://1779454253/AGPE7PI2 - coily exec test Audit-log: coily://1779454274/AGPE7PLK - coily git add backend/main.py tests/test_modes.py |
||
|---|---|---|
| .claude | ||
| .coily | ||
| .github/workflows | ||
| .vscode | ||
| backend | ||
| deploy | ||
| docs | ||
| tests | ||
| todos | ||
| .dockerignore | ||
| .gitignore | ||
| .pre-commit-config.yaml | ||
| .python-version | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| coily.yaml | ||
| Dockerfile | ||
| Makefile | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
backend
A generic data-accessibility framework. An internal operational backend with Postgres behind it, exposing a small set of uniform modes, each a FastAPI router with a generic endpoint and a self-documenting sentinel record. Internal-only - reachable on the tailnet via an in-Pod Tailscale sidecar, no public ingress. Design: coilysiren/backend#77, coilysiren/backend#65, coilysiren/agentic-os-kai#657.
Deploys to the k3s homelab via the canonical rig in infrastructure/docs/k3s-deploy-notes.md.
Modes
Each mode lives in backend/modes/, owns one table, and ships an APIRouter plus an async init(pool) that creates its schema and upserts a sentinel row. Adding a mode is a new module plus an entry in ALL_MODES - nothing else changes.
- health -
GET /health: DB connectivity, the mounted modes, and every sentinel. The only unauthed route - it is the liveness probe. - document - append-only jsonb document store keyed by
(namespace, key).POST /document,GET /document/{namespace},GET /document/{namespace}/{key},DELETE /document/{namespace}/{key}. - queue -
SELECT ... FOR UPDATE SKIP LOCKEDwork queue.POST /queue/{namespace}enqueue,POST /queue/{namespace}/claimclaim,DELETE /queue/{namespace}/{id}ack.visible_atcarries enqueue delay and post-claim visibility timeout. - sql - generic relational mode.
POST /sql/tablescreates a real typed tablesql_<name>from a column spec,GET /sql/tableslists the registry,POST /sql/tables/{name}/rowsandGET /sql/tables/{name}/rowsCRUD its rows. Strict identifier regex plus a closed type allowlist - the "dispatch a new table type from mobile" surface. - file - temp-tier file storage.
POST /files/tempwrites a raw body to an ephemeralemptyDir, returns an id. Write-only by design - no read route in v1.
Sentinel pattern
A shared sentinels(mode, shape jsonb, note, created_at) table. Each mode upserts one row on init describing its record shape - the ".keep-of-schemas" exemplar that keeps the framework self-documenting. GET /health returns them all.
Auth
Every mode route except /health requires an Authorization: Bearer <DATASTORE_TOKEN> header. The token lives in AWS SSM at /coilysiren/backend/datastore-token. Auth fails closed - with no token configured the routes return 503 rather than open up.
Install
brew install uv jq
brew install --cask docker
Environment
Create .env:
DATASTORE_TOKEN=dev-token # bearer token every mode route validates
DATABASE_URL=postgresql://backend:backend@localhost:5432/backend
FILE_TEMP_DIR=/tmp/backend-files # temp tier for the file mode
OTEL_SDK_DISABLED=true
DATABASE_URL can be left unset and assembled from PGHOST / PGUSER / PGPASSWORD / PGDATABASE / PGPORT instead, which is how the k8s manifest injects it.
A local Postgres for development:
docker run -d --name backend-db -p 5432:5432 \
-e POSTGRES_USER=backend -e POSTGRES_PASSWORD=backend -e POSTGRES_DB=backend \
postgres:17
Run
make build-native # uv lock + uv sync
make run-native # uvicorn on :4000
curl -s http://localhost:4000/health | jq
curl -s -X POST http://localhost:4000/document \
-H "Authorization: Bearer dev-token" \
-H "Content-Type: application/json" \
-d '{"namespace":"ci-status","key":"demo","payload":{"status":"ok"}}' | jq
curl -s http://localhost:4000/document/ci-status/demo \
-H "Authorization: Bearer dev-token" | jq
Test
make test
Pure tests run without a database. The DB-integration tests are skipped unless BACKEND_TEST_DATABASE_URL points at a throwaway Postgres.
Commands
Dev commands are declared in .coily/coily.yaml. Run them as coily exec <verb>.
See also
- AGENTS.md - agent-facing operating rules.
- docs/FEATURES.md - inventory of what ships today.
- .coily/coily.yaml - allowlisted commands. Agents route through coily, not bare
make/uv/python/npm/cargo/dotnet.
Cross-reference convention from coilysiren/agentic-os-kai#313.