Skip to content

chantraio/chantra

Chantra Backend

A minimalist production-ready backend MVP split into four independent apps:

  • Landing pages (cmd/landing-pages)
  • REST API (cmd/rest-api)
  • MCP server (cmd/mcp-server)
  • Channel connections service (cmd/channel-connections)

Stack

  • Go 1.24.0+
  • PostgreSQL
  • MinIO (S3-compatible payload storage)
  • GORM
  • MCP: github.com/mark3labs/mcp-go
  • Tests: github.com/stretchr/testify

Run with Docker

make docker-up

Services:

  • Landing pages: http://localhost:8080
  • REST API: http://localhost:8081
  • MCP endpoint (Streamable HTTP): http://localhost:8090/mcp
  • MCP compatibility alias: http://localhost:8090/sse
  • MCP discovery endpoint: http://localhost:8090/.well-known/mcp-server
  • MCP health endpoint: http://localhost:8090/healthz
  • Channel connections (SSE + internal MCP read backend): http://localhost:8070
  • Postgres: localhost:5432
  • MinIO API: http://localhost:9000
  • MinIO Console: http://localhost:9001

Channel URLs returned by create channel operations use CHANNEL_HOSTNAME (not REST/MCP bind address), e.g. https://hostname.io/channel/v1/public/{channel_id}.

Stop:

make docker-down

Deploy to Prod (Docker Context)

Deploy using docker context pdfturn:

TOKEN_SECRET_SALT=replace-me make deploy-prod

Teardown:

make deploy-prod-down

Notes:

  • Production compose file: docker-compose.prod.yml
  • Caddy config: deploy/caddy/Caddyfile
  • Caddy routes:
    • api.chantra.io -> rest-api
    • mcp.chantra.io -> mcp-server
    • chantra.io -> landing-pages
    • /channel/* on chantra.io -> channel-connections

Local Run

  1. Start Postgres and MinIO (for example, with docker compose up postgres minio minio-init -d)
  2. Copy .env.example to .env and fill in the secrets
  3. Apply migrations:
make migrate-up
  1. Start services:
make run-rest
make run-mcp
make run-channel-connections
make run-landing

Main Commands

  • make run
  • make run-landing
  • make run-all
  • make run-rest
  • make run-mcp
  • make run-channel-connections
  • make build
  • make test
  • make test-unit
  • make test-integration
  • make lint
  • make migrate-up
  • make migrate-down
  • make docker-up
  • make docker-down
  • golangci-lint run
  • govulncheck ./...

HTTP API

Health:

  • GET /healthz

Channels:

  • POST /v1/channels
  • GET /v1/channels/{channelID}
  • DELETE /v1/channels/{channelID}
  • POST /v1/channels/{channelID}/publish

Read grants:

  • POST /v1/channels/{channelID}/read-grants
  • POST /v1/channels/{channelID}/read-grants/{grantID}/revoke

Read messages:

  • REST API does not expose message-read endpoints.
  • Public/direct reads use channel URL SSE through https://.../channel/v1/{public|private}/{channel_id}.
  • The channel URL path itself is the SSE endpoint when resolved through HTTPS, e.g. https://chantra.io/channel/v1/public/{channel_id}.
  • channel-connections serves this as GET /v1/{public|private}/{channelID} (via edge /channel/*), supports Last-Event-ID.
  • Private-channel read auth headers: X-Read-Token or Authorization: Bearer <token>.
  • MCP reads use read_channel_messages, which calls channel-connections internal endpoint GET /internal/v1/channels/{channelID}/messages.

Notes:

  • No users, no email, and no auth/registration endpoints.
  • POST /v1/channels is public and returns one-time publish_token (read_token also returned for private channels).
  • Management/publish operations require header X-Publish-Token.
  • Read and publish tokens are different.
  • Global rate limiting returns HTTP 429.
  • Channel lifecycle: channels with no published message activity for 30 days are deleted with related data.

MCP tools

  • create_channel
  • delete_channel
  • publish_message
  • create_read_grant
  • revoke_read_grant
  • get_channel
  • read_channel_messages (uses channel-connections service)

Notes

  • Only token hashes (publish key, read grant) are stored in DB.
  • Raw tokens are returned only at creation time.
  • Message cursoring uses sortable message IDs (UUIDv7), not per-channel sequence IDs.
  • Messages expired by TTL are not returned when reading
  • Binary payload is stored in MinIO, and only payload_object_key is saved in the DB
  • In the API, payload is returned as payload
  • Maximum message TTL: 48h (172800 seconds)
  • Lifecycle-based object deletion is enabled for the MinIO bucket after 2 days

Migration policy

  • Production services run with AUTO_MIGRATE=false.
  • Run migrations explicitly before deploying services.
  • Down migrations can be destructive and should be run only in controlled rollback procedures.

Security

License

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages