GitHub - Cyoda/cyoda-go: Cyoda-Go: The Enterprise EDBMS

9 min read Original article ↗

CI Latest release Go Reference License

One transactional runtime for the entity lifecycle.

cyoda-go is an EDBMS (Entity Database Management System) — state machine, processors, and full revision history live inside the record, committed atomically. Minimizes the need for sagas, CDC pipelines, and external orchestration.

Correctness over availability. When the two conflict, cyoda-go fails closed: an operation that cannot be completed correctly is rejected, never committed partially or with a substituted value. A required dependency being unavailable fails the operation rather than degrading it.

Four storage engines, one application contract

Same application code, four operational shapes:

Engine Where it fits Availability
memory Local dev, unit tests, digital-twin scenarios open source
sqlite Edge, single-node self-host, persistent dev open source
postgres Production transactional workloads, HA open source
cassandra Distributed scale, high write throughput commercial (Cyoda)

Switch by setting CYODA_STORAGE_BACKEND — no code changes. The cassandra engine is offered as a commercial backend by Cyoda for workloads that outgrow a single PostgreSQL primary; contact information is on the cyoda.com website.

Try it in 30 seconds

brew install cyoda/cyoda-go/cyoda
cyoda init && cyoda &
curl http://localhost:8080/api/health
# {"status":"UP"}

cyoda init writes a sqlite-backed user config (default path ~/.local/share/cyoda/cyoda.db); cyoda then starts the server with that config and mock auth. See Install for non-Homebrew options and First real call for jwt + a real authenticated request.

Install

Homebrew (macOS / Linux)

brew install cyoda/cyoda-go/cyoda

curl (any Unix)

curl -fsSL https://github.com/cyoda/cyoda-go/releases/latest/download/install.sh | sh

Installs to ~/.local/bin/cyoda and runs cyoda init. Pin a version with CYODA_VERSION=v0.7.1 curl ... | sh. The installer SHA256-verifies the archive and, if cosign is on PATH, also verifies a Sigstore keyless signature from the cyoda-go release workflow.

Debian / Ubuntu / Fedora / RHEL

# Debian / Ubuntu
wget https://github.com/cyoda/cyoda-go/releases/latest/download/cyoda_linux_amd64.deb
sudo dpkg -i cyoda_linux_amd64.deb

# Fedora / RHEL
wget https://github.com/cyoda/cyoda-go/releases/latest/download/cyoda_linux_amd64.rpm
sudo rpm -i cyoda_linux_amd64.rpm

Replace amd64 with arm64 for ARM hosts. Both packages drop /usr/bin/cyoda and /etc/cyoda/cyoda.env (sqlite as the system-wide default, preserved across upgrades).

From source

Requires Go 1.26+.

go install github.com/cyoda-platform/cyoda-go/cmd/cyoda@latest

This binary uses the in-memory backend by default. Run cyoda init for sqlite persistence, or set CYODA_STORAGE_BACKEND directly.

First real call

The 30-second example uses mock auth. To exercise the real auth chain end-to-end with sqlite + jwt — without leaking the bootstrap secret into your shell history or ps output — use the project's profile pattern:

# Generate a JWT signing key (openssl writes it 0600 by default; make it explicit)
openssl genrsa -out /tmp/jwt.key 2048
chmod 600 /tmp/jwt.key

# Write a local profile with sqlite + jwt + bootstrap creds. .env.local is
# gitignored; chmod 600 keeps the secret off other users' eyes on shared boxes.
cat > .env.local <<'EOF'
CYODA_STORAGE_BACKEND=sqlite
CYODA_IAM_MODE=jwt
CYODA_JWT_SIGNING_KEY_FILE=/tmp/jwt.key
CYODA_BOOTSTRAP_CLIENT_ID=demo
CYODA_BOOTSTRAP_CLIENT_SECRET=demo-secret
EOF
chmod 600 .env.local

# Start cyoda with the local profile (loads .env.local automatically)
CYODA_PROFILES=local cyoda &

# Read the secret from the file at the moment we need it — never `export` it
SECRET=$(grep '^CYODA_BOOTSTRAP_CLIENT_SECRET=' .env.local | cut -d= -f2-)

# Get an OAuth 2.0 token via client_credentials
TOKEN=$(curl -sX POST http://localhost:8080/api/oauth/token \
  -u "demo:$SECRET" \
  -d "grant_type=client_credentials" | jq -r .access_token)

# Make an authenticated call
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/account

The /api/account response confirms the bootstrap client's tenant and roles. From here, follow the Build an app link below to register an entity model and start creating entities.

Optional IAM feature flags (all default false):

Env var Default Effect
CYODA_IAM_TRUSTED_KEY_REGISTRATION_ENABLED false When true, enables the 5 /oauth/keys/trusted/* admin endpoints. When false, those endpoints return 404 FEATURE_DISABLED.
CYODA_IAM_M2M_ADMIN_ROLE_ENABLED false When true, POST /clients?withAdminRole=true may grant ROLE_ADMIN to created M2M clients. When false (default), that request shape returns 404 FEATURE_DISABLED.

In mock mode, CYODA_IAM_MOCK_KIND (default user) sets the principal kind (user/service/system) on the mock default UserContext, so local/CI setups can exercise service- or system-attributed code paths without real JWT auth.

Federated OIDC providers

cyoda-go can accept JWTs issued by external OIDC providers — Auth0, Cognito, Keycloak, or any spec-compliant issuer — alongside its own first-party tokens. Each tenant registers its own providers; tokens are validated against the provider's JWKS endpoint.

Validation order: cyoda-go tries the built-in JWKSValidator first (trusted keys registered via /oauth/keys/trusted/*), then the OIDCValidator. The first validator that recognises the issuer wins.

Management endpoints (JWT mode, CYODA_IAM_MODE=jwt):

Method Path Auth
POST /oauth/oidc/providers ROLE_ADMIN
GET /oauth/oidc/providers any authenticated tenant member
PATCH /oauth/oidc/providers/{id} ROLE_ADMIN
POST /oauth/oidc/providers/{id}/invalidate ROLE_ADMIN
POST /oauth/oidc/providers/{id}/reactivate ROLE_ADMIN
DELETE /oauth/oidc/providers/{id} ROLE_ADMIN
POST /oauth/oidc/providers/reload ROLE_ADMIN

The reload endpoint flushes the in-memory JWKS cache and re-fetches keys from every active provider for the tenant — useful after a key rotation at the IdP.

Register a provider:

curl -sX POST http://localhost:8080/api/oauth/oidc/providers \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-auth0",
    "wellKnownConfigUri": "https://example.auth0.com/.well-known/openid-configuration",
    "audienceClaim": "https://api.example.com",
    "rolesClaim": "https://example.com/roles"
  }'

Configuration: see cyoda help config.auth (the "Federated OIDC providers" section) for the six CYODA_OIDC_* env vars that control HTTPS enforcement, SSRF blocking, default roles claim, and HTTP timeouts for discovery and JWKS fetches.

Composite unique keys

An entity model can declare one or more composite unique keys — each key is a set of scalar field paths that must be unique across all live entities of that model within a tenant.

Declaring keys requires the model to be UNLOCKED:

PUT /api/model/{entityName}/{modelVersion}/unique-keys

Request body:

{
  "uniqueKeys": [
    { "id": "by-email", "fields": ["$.email"] },
    { "id": "by-org-and-handle", "fields": ["$.org", "$.handle"] }
  ]
}

This call is idempotent — it replaces the model's entire key list. The keys are validated immediately (field paths must be known scalar leaves in the inferred schema). After locking the model, no entity create or update can produce a duplicate value-set for any declared key.

Key semantics:

  • Scope: per (tenant, model name, model version), live entities only. Soft-deleting an entity frees its key value-set.
  • Null rule (all-or-nothing): if all fields in a key are absent or null, the entity is exempt. If some but not all fields are present, the write is rejected with 422 INVALID_UNIQUE_KEY. If all fields are present, uniqueness is enforced.
  • String comparison is byte-exact: case-sensitive, no Unicode normalization, no whitespace trimming — the bytes the application wrote are what is compared. Applications that want case-insensitive matching must normalize before writing.
  • Enforced on create and update. Moving a key value to a free slot is allowed; moving it to a slot already taken by another entity returns 409 UNIQUE_VIOLATION.
  • Supported backends: memory, sqlite, postgres. The commercial backend returns 422 COMPOSITE_KEY_UNSUPPORTED until its own support lands.

Multi-node note: see the cluster help topic — Composite unique key staleness — for a bounded operational limitation when changing a key on a live multi-node postgres deployment.

Search result sorting

Search endpoints accept one or more sort query parameters to order results by scalar data or meta fields:

POST /api/search/direct/{entityName}/{modelVersion}?sort=price:asc&sort=@creationDate:desc

Grammar: [@]path[:asc|desc] — a bare dotted path sorts by a scalar entity-data field; the @ prefix sorts by a meta field. Direction defaults to asc. Repetition order is sort precedence; entity_id is always the final tiebreaker. Absent/null values sort last.

Sortable meta fields: state, creationDate, lastUpdateTime, transitionForLatestSave, transactionId, id.

Error: unsortable, unknown, or non-scalar paths return 400 INVALID_FIELD_PATH.

Key cap: CYODA_SEARCH_MAX_SORT_KEYS (default 16) — see cyoda help config (Search and transaction internals).

Scheduled transitions

A workflow transition with a schedule fires automatically after a delay, driven by a coordinator-only scan loop rather than a manual trigger. The delay can be a static delayMs, or a function callout computing the firing time (and optional expiry) per entity at arm time — mutually exclusive with delayMs. See cyoda help config.scheduler for the full topic.

Env var Default Effect
CYODA_SCHEDULER_ENABLED true Kill switch for the scan loop.
CYODA_SCHEDULER_SCAN_INTERVAL 1s Coordinator scan cadence.
CYODA_SCHEDULER_BATCH_SIZE 100 Max due tasks pulled per scan.
CYODA_SCHEDULER_DISTRIBUTION round-robin Dispatch-target strategy: round-robin or self. Forced to self when CYODA_CLUSTER_ENABLED=false.
CYODA_SCHEDULER_COORDINATOR lowest-node-id Coordinator-election strategy.
CYODA_SCHEDULER_REDISPATCH_BACKOFF 30s Best-effort re-dispatch throttle window after a due task is picked up.
CYODA_SCHEDULER_EXPIRY_GRACE 100ms Grace band above a transition's timeoutMs before it expires instead of firing late; size to at least the max inter-node clock skew.

Where to go next

Online docs at docs.cyoda.net mirror the cyoda help topic tree — the same content is available offline via cyoda help <topic>.

Run cyoda help config all for the complete env-var reference (add --format=json for machine-readable output); cyoda help config cluster covers multi-node/dispatch vars.

Goal Link
Build an app fast (Claude Code) github.com/cyoda/cyoda-skills — install the cyoda-skills plugin and use /cyoda:app to scaffold
Build an app docs.cyoda.net/help/quickstart
Configure docs.cyoda.net/help/config
Error reference docs.cyoda.net/help/errors
Deploy with Helm docs.cyoda.net/help/helm
Deploy with Docker Compose examples/compose-with-observability/
Architecture docs/ARCHITECTURE.md
Application examples docs/PRD.md#target-applications
Product overview docs/PRD.md
Feature & API inventory docs/FEATURES.md
Multi-node cluster docs.cyoda.net/help/cluster
Admin endpoints (log/trace) docs.cyoda.net/help/admin
Write a storage plugin docs/plugins.md
Contribute CONTRIBUTING.md
Security disclosures SECURITY.md

Related projects

Sibling repositories under github.com/cyoda that complement cyoda-go:

  • cyoda-skills — Claude Code skills (/cyoda:app, /cyoda:design, /cyoda:build, /cyoda:test, ...) for AI-assisted Cyoda app development against a local cyoda-go or Cyoda Cloud instance.
  • cyoda-cloud-cli — Command-line client for Cyoda Cloud with OAuth 2.0 authentication and Cloud-side API operations.
  • cyoda-docs — Source for docs.cyoda.net — developer guides, onboarding, and the rendered cyoda help topic tree.
  • cyoda-workflow-editor — TypeScript components for parsing, rendering, and editing Cyoda workflow JSON definitions.

Versioning

The Cyoda-Go ecosystem follows Semantic Versioning with a leading v, under the pre-1.0 convention where the minor component is the breaking-change signal:

  • 0.MINOR.0 — a backward-incompatible change to the cyoda-go binary's public contract: the HTTP/wire API.
  • 0.x.PATCH — any backward-compatible change, including new features: additive API parameters, new endpoints, new optional SPI fields, and bug fixes all ship as patches.

This is the "leftmost non-zero component is the de-facto major" convention (as used by Cargo and npm's ^0.x ranges). It keeps the minor counter meaningful — a minor bump means "something under you may have broken" — rather than a feature odometer. The discipline it rests on: a breaking change never ships in a patch.

Each module versions on its own axis. cyoda-go-spi, the cyoda-go binary, the in-tree plugins, and the Helm chart are not required to share a version number. The compatible combinations are recorded in COMPATIBILITY.md; that matrix, not a shared digit, is the source of truth for what works with what.

cyoda-go-spi is the exception, because it is effectively an internal library: its only consumers are the in-tree plugins and the commercial Cassandra backend, all released in lock-step with the binary. It therefore ships breaking interface changes in patch releases rather than burning a minor for an audience of two. Consumers must read the SPI's own ### Breaking changelog section on every bump — for that module the version component is not the breakage signal.

See CHANGELOG.md for breaking changes and MAINTAINING.md for the policy on older release lines.

License

Apache-2.0 — see LICENSE.