Service SDK
Service SDK turns an ESS-backed service definition into a deterministic, event-sourced Rust service. From one reviewed package it generates the service implementation, executable host, client contracts, OpenAPI, service catalogue, conformance scenarios, documentation, Connector factories, and an ESS-native independent release unit.
Use it when you want service behavior to stay reviewable as intent while authentication, authorization, persistence, projections, and generated clients remain consistent by construction.
Service SDK is an Apache-2.0 licensed developer preview. Its Rust crates and Vue component are consumed from exact Git revisions; they are not published to crates.io or npm.
Build and test
Prerequisites are Rust 1.91 or newer, Node.js 22, pnpm 10.15, the task runner, the AEP CLI,
Docker and PostgreSQL client tools. The complete gate requires an explicitly supplied disposable
PostgreSQL fixture with verified TLS, a separate migration role, and a DML application role with
connection limit 4. The required proof workflow
contains the complete reproducible fixture setup.
git clone https://github.com/beyond10x/service-sdk.git
cd service-sdk
corepack enable
# Set the four fixture inputs from your disposable PostgreSQL setup:
# EVENTLOG_TEST_HOSTED_POSTGRES_URL application-role URL
# EVENTLOG_TEST_POSTGRES_MIGRATION_URL migration-role URL
# EVENTLOG_TEST_POSTGRES_CA path to its CA PEM
# EVENTLOG_TEST_POSTGRES_CONTAINER exact disposable Docker container name
task --concurrency 1 check
task check formats, lints, builds documentation, tests every Rust crate, validates the AEP
artifacts, checks the Vue service console, and verifies generated persistence fixtures. Missing
PostgreSQL inputs, filtered proof cases, skipped cases and an incomplete workload matrix fail.
The proof stops and restarts only the explicitly named disposable container. Run it against a
dedicated fixture: it creates retained test schemas and exercises database loss and pool pressure.
Consume the SDK
Pin the SDK commit in the consuming workspace so generation and runtime compilation use the same reviewed source:
[dependencies]
service-builder = { git = "https://github.com/beyond10x/service-sdk.git", rev = "<commit>" }
service-engine = { git = "https://github.com/beyond10x/service-sdk.git", rev = "<commit>" }
service-runtime = { git = "https://github.com/beyond10x/service-sdk.git", rev = "<commit>" }
A service repository supplies a unified service/1 or service/2 package and gives the generated tree exclusive
ownership of its output directory:
cargo run --locked -p service-builder -- \
generate --package service.yaml --output generated/service
cargo run --locked -p service-builder -- \
check --package service.yaml --output generated/service
The first command regenerates the complete artifact tree. The second is suitable for CI and fails
on missing, unexpected, or changed generated bytes. AgentIDE's
service.yaml is a working public
consumer.
When the package declares release, the same tree also contains validated ess-component/1,
ess-build/1, ess-realization/1, and ess-runtime/1 sources, their canonical IR, BuildKit
executor inputs, and the component-owned Helm chart. The generated binary delegates environment,
selected persistence, listener, and shutdown behavior to service-host; the service repository does not need a
handwritten host or Dockerfile.
Select persistence
SQLite remains the generated host's default and retains the existing file database and durable
volume. service/2 adds the closed release.persistence selection, either sqlite or postgres.
service/1 rejects that field, including explicit null, and old builders refuse service/2.
The PostgreSQL release supplies application configuration and secret slots through ESS runtime
projection. It does not mount a SQLite volume or give the serving process migration credentials.
For standalone PostgreSQL, set <PREFIX>_PERSISTENCE=postgres. The generated environment prefix
is the uppercase service name with punctuation replaced by underscores. Supply every production
setting explicitly:
Suffix after <PREFIX>_ | Required value |
|---|---|
POSTGRES_URL, POSTGRES_SCHEMA, POSTGRES_CA_PEM | DML connection URL, owner schema, and trusted CA PEM bytes. TLS certificate and hostname verification are mandatory. |
POOL_MAX, POOL_WAITERS | Finite connection and waiting-request bounds for one process. |
ACQUISITION_MS, CONNECT_MS, STATEMENT_MS, LOCK_MS, TRANSACTION_MS, SHUTDOWN_MS | Positive, bounded timeouts in milliseconds. |
DATABASE_CONNECTIONS, REPLICAS, RESERVED_CONNECTIONS | Verified allocation, replica count and migration/operations reserve. The provider checks the declared budget and the application role. |
DRAIN_MS | Positive HTTP drain deadline in milliseconds. |
LISTEN, IDENTITY_ORIGIN | Listener address and the existing Identity origin. |
More columns: swipe horizontally, or focus the table and use the arrow keys.
Create the deployment-owned schema and DML grants with the migration role. Run the generated
binary with --migrate, supplying POSTGRES_MIGRATION_URL and the same trust, schema, pool and
budget settings. Give the serving invocation only POSTGRES_URL. Application startup validates
the existing schema and role; it does not perform DDL. Readiness reads the database, while
liveness reports whether the process is running. Shutdown closes admissions and attempts the
configured HTTP drain and pool close; an expired deadline reports incomplete shutdown.
A composed host uses service_host::Persistence to migrate and open the same adapter, passing
the complete service roster to migrate_postgres and open_postgres. Inject persistence.store()
into every generated Connector factory, bind all services, then call persistence.seal() once
before listening. An incomplete roster or registration after sealing refuses. Keep the
Persistence handle for readiness, draining and shutdown alongside the injected store.
The declared laboratory profile uses two SDK processes with two connections and four waiters each, a four-connection application role, and a separate four-connection reserve. Its fixed timeouts and six workload configurations are test inputs, not production sizing evidence. The current v3 profile admits steady driver requests through one shared gate, at least one millisecond apart, with measured pacing and request latency reported separately. Its original unpaced v2 sweep remains recorded as rejected after exhausting its request cap before the minimum duration. Independent unpaced saturation and cancellation cases remain required. Actual generated standalone and Connector factory cases run separately from the measured SDK execution boundary. The mandatory proof records complete source/binary hashes, operation counts, latency, provider refusals, sampled waiter-time and process/database resources.
Retry and existing data
New accepted intents atomically record service-intent-claim/1 through Eventlog's existing claim
interface. Retrying the original idempotency identity and input returns the original stream,
UUIDs, events and content references, including a completed transition. Current authentication
and authorization are checked again. A changed input or incompatible service plan refuses.
Receipt recovery reads a bounded original event range; unresolved outcomes never claim success
or admit a new external effect. Effect preparation, claims and terminal outcomes retain their
existing journal identities across restart.
Existing event hashes and stream, feed, cursor, projection storage, effect and service-content/1
encodings remain unchanged. The historical content digest includes tenant, exact optional realm,
policy, idempotency key, media type and bytes; it does not add a service namespace. There is no
public SDK content-download route. Tests cover the older service-stream/1 identity vectors;
no older SQL adapter was located, so SQL adapter compatibility is not claimed for that runtime.
Before admitting writes on an existing deployment, its owner must stop intake, fence every old writer, drain and classify outstanding operations, and reconcile unresolved requests from retained client or owner evidence. Old SDK commands had no original-intent claim. An old lost response containing an unknown generated stream UUID cannot be recovered by the new bounded lookup; it blocks that deployment's write admission until reconciled. This release does not scan tenant feeds, synthesize legacy receipts or prove that a deployment completed this process.
Optional fields and projection upgrades
Typed service-realization-plan/3 views represent an empty optional object field by its absence.
For example, a grant without an expiry omits expires_at from its query row. The accepted command,
stored event and folded state retain the original explicit null; present optional values and
nested nulls remain unchanged. The validator rejects an externally supplied optional null row.
Legacy service-realization-plan/2 keeps its historical null-valued query representation.
This is an explicit query-output change when upgrading an existing plan/2 deployment. Before
candidate traffic, fence all writers, retain a complete closed backup, and rebuild the service's
projections for each tenant with the approved new plan. EventlogService::projector(engine)
constructs the same SDK-owned projector used by serving initialization without registering it
or touching storage. Pass that projector to the provider's create_projections and
rebuild_projection operations in a fresh maintenance process. Its local is_inline check
cannot establish that other processes have stopped. Serving initialization alone does not
rewrite existing rows, and locally inline rebuilds refuse.
Verify complete old and rebuilt views with only the declared top-level optional-null-to-absence mapping, and verify original events, identities, command outcomes and claims independently. Retain the original views; do not normalize their evidence. Reopen the rebuilt store and verify the candidate's queries before admitting traffic. Genuine historical SDK migration and the complete PostgreSQL consumer proof are separate from the SDK's file SQLite plan-format tests.
Why it is built on ESS
ESS owns the semantic model and compiler-minted intermediate representation. Service SDK consumes that validated meaning instead of reconstructing it. This gives adopters:
- one semantic name and schema across generated services, clients, Connectors, and documentation;
- deterministic generation that can be checked byte-for-byte in CI;
- explicit refusals when runtime obligations are missing or incompatible;
- executable conformance scenarios tied to the same service definition.
The SDK never guesses business logic. Every runtime gap must select a closed, versioned obligation provider; application repositories contain definitions and generated output rather than handwritten realization hooks.
What it generates
service/1 or service/2 package
-> ESS fragments + service-definition/3 + scenarios + exact SDK lock
-> compiler-minted EssIr and ESS SynthesisPlan
-> validated service-runtime-ir/3 + versioned obligation catalogue
-> SDK-executable realization plan
-> Rust service + host, typed Identity HTTP client, OpenAPI, service-catalog/1, docs,
scenarios, Connector factories, and ESS component/build/runtime/chart artifacts
The workspace is split by responsibility:
service-definitiondescribes author-facing runtime annotations against ESS semantic names.service-runtime-iris the closed, digest-bound realization contract.service-obligationsprovides versioned runtime implementations and coverage checks.service-runtimesupplies transport-independent authenticated execution and durable effects.service-engineexecutes generated plans over deployment-injected resources.service-eventlogprovides the Eventlog-backed persistence adapter.service-httpprovides the Identity-authenticated HTTP server and generated-client transport.service-hostprovides the generated binary's environment, SQLite/PostgreSQL composition, startup roster, listener, readiness and bounded shutdown.- Identity HTTP packages expose that host through the default
standalone-hostfeature. Client-only consumers disable default features so they do not resolve the process host or persistence adapter. service-connectorsexposes inert factories for composed Connector runtimes.service-catalogpublishes the generated operation catalogue.service-buildergenerates and drift-checks the complete artifact tree.service-conformanceproves the generated surfaces stay one operation contract.@b10x/service-console-vuerenders the generated catalogue through an explicit host binding.
Security and authority
Authentication establishes tenant, authority, user, optional executor, and optional realm before
application input is decoded. Realm is never accepted from a route, query, body, caller-controlled
header, generated-client argument, or Connector coordinate. None and Some("default") remain
distinct.
An identity_http service definition declares one exact resource audience and every operation
declares one exact OAuth scope. The generated server validates both at the Identity boundary before
decoding application JSON; the generated client is permanently bound to that audience. A
composed_connector service keeps the existing in-process Connector delivery instead. Version 0.4
accepts only the new /3 definition and runtime formats and intentionally provides no compatibility
reader for prior generated artifacts.
Accepted mutations pass through authenticated intent, authorization and validation, semantic command decision, guarded Eventlog append, reduction, and the declared projection guarantee. External effects use digest-sealed, durable plans and record uncertain transport outcomes rather than repeating them blindly.
Aggregate ownership and projection visibility are separate decisions. Services use
sdk.auth.owner-and-conjunctive-scopes/v1 to keep mutations owner-bound. A shared read model may
instead select sdk.projection.conjunctive-scopes-visibility/v1: Eventlog still partitions every
query by the Identity-derived tenant and exact optional realm, while the row is visible to any
principal admitted by all populated scope axes. Reading a shared row never confers mutation
authority.
See SECURITY.md for private vulnerability reporting and CONTRIBUTING.md before proposing a change.
Licence
Apache-2.0. See LICENSE.