One port, three protocols, one client object
Loams’s native API is one set of protobuf services served as Connect, gRPC and gRPC-Web on the same port, beside health and reflection. A curl POST of JSON, a grpcurl call and a browser’s gRPC-Web fetch are the same RPC. This page is the reference for that surface: the 53 methods that are registered, the 52 reasons a call can fail with, the REST routes that are still there beside them, and the SDKs.
The native API, in full.
Scalar, rendered from a protobuf-derived OpenAPI snapshot aligned with current handlers.
A local engine listens on `127.0.0.1:8080`. Every example on this page uses that address. There are no hosted endpoints yet. `LOAMS_AUTH_MODE` picks authentication: `off` (the default, which refuses to start unless every listener is on loopback), `apikey` (`Authorization: Bearer lk_…`), `oidc` (a JWT from a configured issuer), `tailscale`, or a comma list such as `oidc,apikey`. `GetInstance` and the health probes never need a credential.
The reference below is generated from proto/loams/instance/v1/instance.proto,proto/loams/collection/v1/collection.proto,proto/loams/collection/v1/document.proto,proto/loams/collection/v1/query.proto, proto/loams/live/v1/live.proto, proto/loams/errors/v1/errors.proto and the standard gRPC health and reflection contracts. The original schemas use protoc-gen-connect-openapi. Collection/document/Search entries are derived from the current protobuf source and checked against the implemented handlers. It shows the Connect JSON mapping, so every unary call is a POST to /<package>.<Service>/<Method>. Every operation carries a status badge: Available, In progress or Planned.
It loads on request, so none of the reference’s own JavaScript is part of this page’s first load. The description itself is at /openapi/connect.yaml.
Scalar, rendered from the OpenAPI description generated out of the protos. Open it in its own page or read connect.yaml.
What is registered, and what answers.
The difference between the two is the current state of this API.
| Package | SDK module | In this build | Note |
|---|---|---|---|
loams.instance.v1 | instance | Available | Served by every build variant. `GetInstance` answers without a credential; `WhoAmI` reports the authenticated principal, or `dev-loopback` when authentication is off. |
loams.collection.v1 | collectionsdocumentssearchvector | Available | 18 implemented namespace, collection, document and hybrid Search RPCs. They share the REST service layer and carry typed errors, consistency tokens and idempotency on document writes. |
loams.live.v1 unstable | livetables | Planned | Registered with a stub, not left unrouted, so a caller gets `unimplemented` with reason `feature_not_in_variant` and the variant in `metadata` instead of a bare 404. The Live engine, in the default build, is served on its own loopback listener (`127.0.0.1:7710`), with Query, Mutate, Watch and bundle Deploy, not through this main-port stub. Its wire contract may still change, so it is excluded from breaking-change checks. |
loams.graph.v1 unstable | graph | In progress | Loams Graph: stored property graphs queried with GQL. Served with the `graph` build feature (the `full` variant) while the graph runtime is on; otherwise every RPC answers `feature_not_in_variant`. `RestoreGraph`, `ExportGraph` and `ImportGraph` answer `not_implemented`. Unstable. |
loams.house.v1 unstable | house | In progress | Loams House: ClickHouse SQL over Iceberg tables. Not served by this port itself: every `/loams.house.v1.*` path is proxied to the House front named by `--house-endpoint`, and the package is listed as available only while that front is configured and healthy. Without one, every RPC answers `unimplemented` with reason `house_not_configured`. Unstable. |
| RPC | Shape | Status | What it does today |
|---|---|---|---|
loams.collection.v1.NamespaceService/CreateNamespace | unary | Available | Creates a namespace through the shared metastore. |
loams.collection.v1.CollectionService/CreateCollection | unary | Available | Creates a collection; replay by name returns the same collection when the schema agrees. |
loams.collection.v1.CollectionService/ListCollections | unary, GET-able | Available | Lists namespace collections with page tokens. |
loams.collection.v1.CollectionService/GetCollection | unary, GET-able | Available | Returns collection schema, manifest, hot tier and backpressure information; accepts aliases. |
loams.collection.v1.CollectionService/DropCollection | unary | Available | Drops a collection through the shared service. |
loams.collection.v1.CollectionService/AddFields | unary | Available | Adds schema fields with version validation. |
loams.collection.v1.CollectionService/ListVersions | unary, GET-able | Available | Lists retained collection manifest versions. |
loams.collection.v1.CollectionService/Scan | unary | Available | Pins a Lance scan plan for external readers, including its consistency token. |
loams.collection.v1.CollectionService/UpdateAliases | unary | Available | Applies collection alias actions. |
loams.collection.v1.CollectionService/SetHot | unary | Available | Sets collection hot-tier configuration and returns local status. |
loams.collection.v1.CollectionService/WarmCollection | unary | Available | Starts warming the collection on its owner. |
loams.collection.v1.DocumentService/WriteDocuments | unary | Available | Atomically writes document operations; idempotency keys replay recorded results. Returns token and backlog state. |
loams.collection.v1.DocumentService/GetDocuments | unary, GET-able | Available | Returns requested documents in order with token-aware consistency. |
loams.collection.v1.DocumentService/ScrollDocuments | unary, GET-able | Available | Pages documents by primary key with token-aware consistency. |
loams.collection.v1.DocumentService/CountDocuments | unary, GET-able | Available | Counts documents matching a filter. |
loams.collection.v1.DocumentService/DeleteByFilter | unary | Available | Deletes matching documents from a pinned snapshot, with explicit partial-write cursors. |
loams.collection.v1.DocumentService/PatchByFilter | unary | Available | Patches matching documents from a pinned snapshot; filter writes do not create new documents. |
loams.collection.v1.QueryService/Search | unary, GET-able | Available | Hybrid search with typed retrievers, filter IR, fusion, sort, projection, highlights and groups; returns read token and shares the REST search engine. |
loams.instance.v1.InstanceService/GetInstance | unary, GET-able | Available | Instance discovery, alongside the implemented collection and document RPCs. No credentials: an app calls it before sign-in, to learn the instance id, the edition, the API packages and how to sign in. |
loams.instance.v1.InstanceService/WhoAmI | unary, GET-able | Available | Reports the caller: the OIDC user, the API key or the Tailscale user, with its kind and display name. With authentication off every caller is `dev-loopback`. |
loams.live.v1.LiveService/Query | unary | Planned | Answers `unimplemented` with reason `feature_not_in_variant`. Exposed to the SDK as `loams.tables.query`. |
loams.live.v1.LiveService/Mutate | unary | Planned | Same refusal. Takes an `idempotency_key`, so a retried call returns the recorded commit instead of running again. Exposed as `loams.tables.mutate`. |
loams.live.v1.LiveService/Deploy | unary | Planned | Same refusal. Admin-only, and deliberately not annotated with a facade option, so it stays out of every SDK. Exposed as `loams.tables.deploy`. |
loams.live.v1.LiveService/Watch | server stream | Planned | Same refusal, on a server stream. Exposed as `loams.live.watch`. |
loams.live.v1.LiveService/ModifyQuerySet | unary | Planned | Same refusal. Exposed as `loams.live.modifyQuerySet`. |
loams.graph.v1.GraphAdminService/GetEngineInfo | unary, GET-able | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/CreateGraph | unary | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/GetGraph | unary, GET-able | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/ListGraphs | unary, GET-able | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/UpdateGraph | unary | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/DeleteGraph | unary | Available | Returns a `loams.operations.v1.Operation`. Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/GetSchema | unary, GET-able | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphAdminService/RestoreGraph | unary | Planned | Registered; answers `unimplemented` with reason `not_implemented`. |
loams.graph.v1.GraphAdminService/ExportGraph | unary | Planned | Registered; answers `unimplemented` with reason `not_implemented`. |
loams.graph.v1.GraphAdminService/ImportGraph | unary | Planned | Registered; answers `unimplemented` with reason `not_implemented`. |
loams.graph.v1.GraphService/Execute | unary | Available | Runs one GQL statement against a graph. Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphService/ExecuteBatch | unary | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphService/ExecuteStream | server stream | Available | Streams result chunks. Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.graph.v1.GraphService/Explain | unary | Available | Served with the graph build feature and runtime; otherwise `feature_not_in_variant`. |
loams.house.v1.HouseService/ExecuteQuery | server stream | Available | Runs one ClickHouse statement and streams a header, batches, progress and a summary. A statement that writes needs `read_only = false`. Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/CancelQuery | unary | Available | Cancels a running query by id. Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/ExplainQuery | unary, GET-able | Available | Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/ListDatabases | unary, GET-able | Available | Lists the caller namespace’s databases (`default` until the lake catalog lands). Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/ListTables | unary, GET-able | Available | Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/DescribeTable | unary, GET-able | Available | Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/PreviewTable | server stream, GET-able | Available | Streams a table’s first rows. Proxied to the House front; `house_not_configured` without one. |
loams.house.v1.HouseService/ListQueryHistory | unary, GET-able | Planned | Answers `not_implemented` until the query log lands. |
loams.house.v1.HouseService/GetQuery | unary, GET-able | Planned | Answers `not_implemented` until the query log lands. |
loams.house.v1.HouseService/ListPipes | unary, GET-able | Planned | Answers `not_implemented` until pipes land. |
loams.house.v1.HouseService/RestoreTable | unary | Planned | Will return a `loams.operations.v1.Operation`; answers `not_implemented` today. |
grpc.health.v1.Health/Check | unary | Available | The standard gRPC health service. `SERVING` for the whole process, and for each service this build serves. |
grpc.health.v1.Health/Watch | server stream | Available | The health stream, over the same envelope as every other server stream here. |
grpc.reflection.v1.ServerReflection/ServerReflectionInfo | server stream | In progress | Server reflection, mounted in `loams dev` and off elsewhere, so `grpcurl` and Postman can read the contract without a copy of the protos. |
GetInstance.services[] does not list them. The reasons above that name one of their methods are already reserved for it. The app mock server covers them for client development.loams.approvals.v1—ApprovalService:ListApprovals,GetApproval,WatchApprovals,DecideApprovalloams.devices.v1—DeviceService:CreatePairing,ListDevices,RenameDevice,RevokeDevice,RegisterPushTarget,UnregisterPushTarget,GetNotificationPreferences,SetNotificationPreferences,SendTestNotificationloams.notifications.v1—NotificationService:ListNotifications,WatchNotifications,MarkReadloams.postgres.v1—PostgresService:CreateProject,GetProject,ListProjects,UpdateProject,DeleteProject,CreateBranch,GetBranch,ListBranches,UpdateBranch,DeleteBranch,RestoreBranch,SetDefaultBranch,CreateEndpoint,GetEndpoint,ListEndpoints,UpdateEndpoint,StartEndpoint,SuspendEndpoint,RestartEndpoint,DeleteEndpoint,CreateRole,ListRoles,ResetRolePassword,DeleteRole,CreateDatabase,ListDatabases,DeleteDatabase,GetConnectionInfo,IssueConnectCredential,UpgradeProject,WatchProjectloams.operations.v1—OperationsService:GetOperation,ListOperations,WatchOperations,CancelOperation
Branch on the reason, not the message.
Every failure a caller has to handle, in one table.
A failed RPC carries a Connect code and one loams.errors.v1.ErrorInfo in its details. The reason in that detail is the stable, registered string; the message is for a person and may change. The SDKs turn a reason into a typed error class so a caller catches a class rather than parsing anything.
| reason | Connect code | Raised by | metadata |
|---|---|---|---|
approval_expired | failed_precondition | DecideApproval | — |
approval_already_decided | failed_precondition | DecideApproval | — |
approval_stale_revision | failed_precondition | DecideApproval | — |
requester_cannot_approve | permission_denied | DecideApproval | — |
decision_proof_invalid | permission_denied | DecideApproval | — |
step_up_required | unauthenticated | DecideApproval, CreatePairing | — |
reason_required | invalid_argument | DecideApproval | — |
invalid_decision | invalid_argument | DecideApproval | — |
pairing_expired | failed_precondition | the pairing grant | — |
pairing_used | failed_precondition | the pairing grant | — |
device_revoked | unauthenticated | every RPC from a revoked device | — |
push_target_unknown | not_found | UnregisterPushTarget | — |
not_implemented | unimplemented | a stub handler whose service has not landed yet | — |
feature_not_in_variant | unimplemented | any RPC of a catalogue package whose engine is not in this build variant | variant |
house_not_configured | unimplemented | any loams.house.v1 RPC on a server with no [house] endpoint (--house-endpoint): the main port has no Loams House front to proxy to. A front that is configured but unreachable answers unavailable | — |
house_syntax_error | invalid_argument | loams.house.v1 ExecuteQuery, ExplainQuery, PreviewTable: ClickHouse's 62 SYNTAX_ERROR — a statement that does not parse, more than one statement, or a form outside the House's surface | clickhouse_codeclickhouse_name |
house_unknown_table | not_found | loams.house.v1 statements, DescribeTable, PreviewTable: ClickHouse's 60 UNKNOWN_TABLE | clickhouse_codeclickhouse_name |
house_unknown_database | not_found | loams.house.v1 statements and the browsing RPCs: ClickHouse's 81 UNKNOWN_DATABASE | clickhouse_codeclickhouse_name |
house_access_denied | permission_denied | loams.house.v1: ClickHouse's 497 ACCESS_DENIED; a read-only user's write (164); CancelQuery of another caller's query; ListQueryHistory with scope NAMESPACE without house:admin | clickhouse_codeclickhouse_name |
house_read_only | failed_precondition | loams.house.v1 ExecuteQuery with read_only absent or true and a statement that writes or changes state: refused before it runs (ClickHouse's 164 READONLY); the desktop asks to confirm and runs it again with read_only = false | clickhouse_codeclickhouse_name |
house_timeout | deadline_exceeded | loams.house.v1 statements: ClickHouse's 159 TIMEOUT_EXCEEDED — past timeout_ms (or 300 s), or the worker killed 2 s after it | clickhouse_codeclickhouse_name |
house_memory_limit | resource_exhausted | loams.house.v1 statements: ClickHouse's 241 MEMORY_LIMIT_EXCEEDED, or the worker killed over its memory | clickhouse_codeclickhouse_name |
house_too_many_queries | resource_exhausted | loams.house.v1 statements: ClickHouse's 202 TOO_MANY_SIMULTANEOUS_QUERIES — no worker free within the acquire timeout | clickhouse_codeclickhouse_name |
house_quota_exceeded | resource_exhausted | loams.house.v1 statements past a namespace quota | quotalimit |
house_disabled_function | permission_denied | loams.house.v1 statements: ClickHouse's 344 SUPPORT_IS_DISABLED — a table function, function, engine, statement or system table the House's deny list refuses | clickhouse_codeclickhouse_name |
house_cancelled | canceled | loams.house.v1 ExecuteQuery and PreviewTable: ClickHouse's 394 QUERY_WAS_CANCELLED — CancelQuery, or the statement's worker stopped for it | clickhouse_codeclickhouse_name |
house_query_not_found | not_found | loams.house.v1 CancelQuery (no running query of that id) and GetQuery (no logged query of the caller's with that id) | query_id |
house_query_failed | unknown | loams.house.v1 statements: any other error the engine raised whose ClickHouse HTTP status is not 4xx (one whose status is 4xx answers invalid_argument), with its ClickHouse code and name | clickhouse_codeclickhouse_name |
project_not_found | not_found | every loams.postgres.v1 RPC that names a project | project |
branch_has_children | failed_precondition | DeleteBranch | children |
branch_protected | failed_precondition | DeleteBranch, RestoreBranch (an agent's goes through loams.approvals.v1 instead) | branch |
lsn_out_of_retention | failed_precondition | CreateBranch, RestoreBranch | oldest_lsnhistory_retention |
endpoint_exists_for_branch | already_exists | CreateEndpoint (a second read-write endpoint) | endpoint |
compute_start_failed | unavailable | StartEndpoint, RestartEndpoint (the Operation's error) | compute |
quota_exceeded | resource_exhausted | any loams.postgres.v1 create or resize past a limit | quotalimit |
storage_unavailable | unavailable | any loams.postgres.v1 RPC that needs the pageserver, loams-wal or the storage controller while it is down | component |
secret_already_issued | failed_precondition | CreateRole, ResetRolePassword, IssueConnectCredential replayed with the same idempotency_key: the secret is answered once, and the idempotency ledger keeps only that it was issued, never the secret | role |
live_bad_cursor | invalid_argument | loams.live.v1 Query, Mutate and the session's queries: a function's paginate got a cursor that is malformed, altered, or from another app, table or index. Named by the Live engine; reaches the wire once Live moves onto the main Connect port | — |
live_worker_crashed | internal | loams.live.v1 Query, Mutate and the session's queries: the isolated worker process running the function crashed or was killed; the call's transaction never committed, and a mutation is not retried. Live's own wire code is ERROR_CODE_FUNCTION_ERROR; internal is provisional until Live errors are mapped to Connect codes. Named by the Live engine; reaches the wire once Live moves onto the main Connect port | — |
live_deploy_conflict | failed_precondition | loams.live.v1 Deploy: the deploy's expected_active is not the app's active deployment, because another deploy activated in between; the message names both. The deployment's record is failed. Named by the Live engine; reaches the wire once Live moves onto the main Connect port | — |
invalid_argument | invalid_argument | any RPC: a malformed field, an unparseable value | field |
not_found | not_found | any RPC: the named resource does not exist | kindname |
already_exists | already_exists | any RPC that creates a named resource | — |
permission_denied | permission_denied | any RPC the caller's role may not make | — |
token_expired | unauthenticated | a rejected access token | — |
unauthenticated | unauthenticated | any RPC with no or an unusable credential | — |
failed_precondition | failed_precondition | any RPC whose preconditions do not hold | — |
resource_exhausted | resource_exhausted | a backpressure or quota refusal | retry_after_ms |
unavailable | unavailable | a dependency is down or this node cannot serve the read | — |
deadline_exceeded | deadline_exceeded | the caller's deadline passed | — |
aborted | aborted | a concurrent write won; the caller retries | — |
internal | internal | a bug; the message and the request id go to the log | — |
- Branch on `ErrorInfo.reason`. It is `snake_case`, unique, and registered; within an API major version a reason is never renamed and never removed.
- Never branch on the Connect code alone. Several reasons share one code, and a new reason can be added under a code you already handle.
- Never branch on `message`. It is written for a person, in the caller’s locale, and it may change.
- Treat a `reason` you do not know as the code says, not as a bug: the registry only ever grows, and a newer server may raise a reason your SDK has never heard of.
not_found is a NotFoundError in TypeScript. Two reasons get their own class because they are worth catching on their own: feature_not_in_variant becomes a FeatureNotInVariantError carrying .variant, and token_expired a TokenExpiredError, which the runtime reaches only after its one automatic refresh and retry have already failed. Everything else falls through to the base LoamsError.Stable Within an API major version a reason is never renamed and never removed. A bug fix is a new reason; a deprecated one keeps working and stops being returned. The TypeScript SDK compiles this same list into a TypeScript union, so a switch over reason there is checked against every reason that existed when the SDK was generated.
The REST routes that still exist.
Each one has an RPC it becomes. None of them has moved yet.
These routes still answer exactly as they did, on the same port as the Connect API beside them. Resource names are in the path rather than in a request message, which is the one thing that changes when a route becomes an RPC.
| Method | Route | Becomes |
|---|---|---|
| POST | /v1/namespaces | loams.collection.v1.NamespaceService/CreateNamespace |
| POST | /v1/namespaces/{ns}/streams | loams.stream.v1.StreamService/CreateStream |
| GET | /v1/namespaces/{ns}/streams | loams.stream.v1.StreamService/ListStreams |
| GET | /v1/namespaces/{ns}/streams/{stream} | loams.stream.v1.StreamService/DescribeStream |
| POST | /v1/namespaces/{ns}/streams/{stream}/partitions/{partition}/records | loams.stream.v1.StreamService/Produce |
| GET | /v1/namespaces/{ns}/streams/{stream}/partitions/{partition}/records | loams.stream.v1.StreamService/Fetch |
| POST | /v1/namespaces/{ns}/streams/{stream}/events | loams.stream.v1.StreamService/ProduceCloudEvents |
| GET | /v1/namespaces/{ns}/streams/{stream}/partitions/{partition}/events | loams.stream.v1.StreamService/FetchCloudEvents |
| POST | /v1/namespaces/{ns}/links | loams.link.v1.LinkService/CreateLink |
| GET | /v1/namespaces/{ns}/links | loams.link.v1.LinkService/ListLinks |
| GET | /v1/namespaces/{ns}/links/{link} | loams.link.v1.LinkService/DescribeLink |
| POST | /v1/namespaces/{ns}/collections | loams.collection.v1.CollectionService/CreateCollection |
| GET | /v1/namespaces/{ns}/collections | loams.collection.v1.CollectionService/ListCollections |
| GET | /v1/namespaces/{ns}/collections/{c} | loams.collection.v1.CollectionService/GetCollection |
| DELETE | /v1/namespaces/{ns}/collections/{c} | loams.collection.v1.CollectionService/DropCollection |
| POST | /v1/namespaces/{ns}/collections/{c}/fields | loams.collection.v1.CollectionService/AddFields |
| GET | /v1/namespaces/{ns}/collections/{c}/versions | loams.collection.v1.CollectionService/ListVersions |
| POST | /v1/namespaces/{ns}/collections/{c}/scan | loams.collection.v1.CollectionService/Scan |
| POST | /v1/namespaces/{ns}/aliases | loams.collection.v1.CollectionService/UpdateAliases |
| POST | /v1/namespaces/{ns}/collections/{c}/documents | loams.collection.v1.DocumentService/WriteDocuments |
| POST | /v1/namespaces/{ns}/collections/{c}/documents/get | loams.collection.v1.DocumentService/GetDocuments |
| POST | /v1/namespaces/{ns}/collections/{c}/documents/scroll | loams.collection.v1.DocumentService/ScrollDocuments |
| POST | /v1/namespaces/{ns}/collections/{c}/documents/count | loams.collection.v1.DocumentService/CountDocuments |
| POST | /v1/namespaces/{ns}/collections/{c}/documents/delete_by_filter | loams.collection.v1.DocumentService/DeleteByFilter |
| POST | /v1/namespaces/{ns}/collections/{c}/documents/patch_by_filter | loams.collection.v1.DocumentService/PatchByFilter |
| PUT | /v1/namespaces/{ns}/collections/{c}/hot | loams.collection.v1.CollectionService/SetHot |
| POST | /v1/namespaces/{ns}/collections/{c}/warm | loams.collection.v1.CollectionService/WarmCollection |
| POST | /v1/namespaces/{ns}/query | loams.collection.v1.QueryService/Search |
| POST | /v1/namespaces/{ns}/sql | loams.sql.v1.SqlService/Query |
| Method | Route | Becomes |
|---|---|---|
| GET | /health | kept; also grpc.health.v1.Health/Check |
| GET | /ready | kept; also grpc.health.v1.Health/Check |
| Method | Route | Becomes |
|---|---|---|
| POST | /internal/v1/reads/{op} | loams.internal.v1.ForwardService/Read |
| GET | /internal/v1/node/stats | loams.internal.v1.NodeService/GetNodeStats |
| POST | /internal/v1/hot/status | loams.internal.v1.HotService/GetHotStatus |
| POST | /internal/v1/hot/warm | loams.internal.v1.HotService/Warm |
- The console contract (`/api/v1/*`): 47 method and path pairs that exist as an OpenAPI contract served by a mock, not as routes on the engine.
- The metastore’s Raft transport (`/internal/v1/raft/*`, `/internal/v1/meta/*`): node-to-node, on the cluster listener.
- The compatibility surfaces, which keep their own ports and their own wire formats: Qdrant, Elasticsearch, Flight SQL, the Postgres and MySQL wire protocols, MCP and Resonate.
- The planned `loams.postgres.v1` console routes (`/v1/namespaces/{ns}/postgres/projects/…`): no server serves them; `loams pg-control` serves the RPCs only.
- `/loams.house.v1.*`: not a route of this server but a proxy to the House front named by `--house-endpoint`.
Thirteen languages. One runtime contract.
Implemented clients share required conformance fixtures; service coverage and publication have separate milestones.
| Language | Package | Protocol | Distribution target | Wave | Status |
|---|---|---|---|---|---|
| TypeScript / JavaScript | @loams/client | Connect | npm | 1 | Implemented |
| Python | loams | Connect | PyPI | 1 | Implemented |
| Go | loams.dev/go | Connect | Go module proxy | 1 | Implemented |
| Rust | loams | Connect | crates.io | 1 | Implemented |
| Swift | Loams | Connect | SwiftPM | 2 | Implemented |
| Kotlin | dev.loams:loams-kotlin | Connect | Maven Central | 2 | Implemented |
| Java | dev.loams:loams | gRPC | Maven Central | 2 | Implemented |
| C# / .NET | Loams | gRPC | NuGet | 2 | Implemented |
| Dart / Flutter | loams | Connect | pub.dev | 3 | Implemented |
| Ruby | loams | gRPC / Connect | RubyGems | 3 | Implemented |
| PHP | loams/loams | gRPC / Connect | Packagist | 3 | Implemented |
| C++ | loams | gRPC | vcpkg overlay, Conan | 3 | Implemented |
| Objective-C | Loams | gRPC | SwiftPM target only | 3 | Implemented |
TypeScript and JavaScript
@loams/client is the reference the other twelve follow. It is one Loams object with namespaced modules, generated from the proto annotations, and it works in a browser, in Node, in Deno and in Bun because one port serves Connect, gRPC and gRPC-Web. The default transport is Connect over fetch.
import { Loams, apiKey, FeatureNotInVariantError } from '@loams/client';
const loams = new Loams({
endpoint: 'http://127.0.0.1:8080',
auth: apiKey(process.env.LOAMS_API_KEY!), // or envToken(), oidcExchange({…})
});
const info = await loams.instance.getInstance({}); // no auth needed
await loams.system.guard('live'); // costs no RPC once cached
try {
await loams.tables.mutate(request);
} catch (error) {
if (error instanceof FeatureNotInVariantError) {
// This build variant does not carry loams.live.v1.
console.warn(error.variant); // 'standard'
}
}const { served, unavailable } = await loams.system.catalogue();
if (await loams.system.available('live')) {
// loams.live.v1 is in this build.
}
// Guarded, the same check that throws FeatureNotInVariantError the server's
// own refusal maps to, so one catch covers both paths:
try {
await loams.system.guard('live');
} catch (error) {
if (error instanceof FeatureNotInVariantError) {
console.warn(`${error.reason}: ${error.variant}`);
}
}
// The SDK's packages against the server's, as a compatibility report:
const report = await loams.system.version();
console.log(report.protoRev, report.serverVersion, report.missing);import {
LoamsError,
FeatureNotInVariantError,
TokenExpiredError,
isLoamsError,
} from '@loams/client';
try {
await loams.instance.getInstance({});
} catch (error) {
if (error instanceof FeatureNotInVariantError) {
// reason: 'feature_not_in_variant', metadata.variant: 'standard'
} else if (error instanceof TokenExpiredError) {
// The SDK has already refreshed once and retried; this is the second failure.
} else if (isLoamsError(error)) {
const { code, reason, metadata, hint } = error as LoamsError;
// A reason this SDK version does not know is still a reason: the registry
// only grows, so fall through to the code.
}
}// Node, Deno, Bun and browsers: Connect over fetch (the default).
const loams = new Loams({ endpoint });
// Node only: HTTP/2 gRPC, from the /node subpath so a bundler never follows
// the Node transport into a browser build it cannot satisfy.
import { createNodeTransport } from '@loams/client/node';
const overHttp2 = new Loams({ endpoint, transport: createNodeTransport() });
// Browsers: gRPC-Web. Same client code, same typed answers.
import { createGrpcWebTransport } from '@connectrpc/connect-web';
const inBrowser = new Loams({
endpoint,
transport: createGrpcWebTransport({ baseUrl: endpoint }),
});const loams = new Loams({ endpoint, auth: apiKey(key), sessionConsistency: true });
await loams.tables.mutate(request); // its ConsistencyToken is recorded
const rows = await loams.tables.query(request); // carries the merged token
// Or per call, without the session:
await loams.tables.query(request, { consistency: 'EVENTUAL' });| Module | Service | Calls |
|---|---|---|
loams.instance | loams.instance.v1.InstanceService | getInstancewhoAmI |
loams.live | loams.live.v1.LiveService | watchmodifyQuerySet |
loams.tables | loams.live.v1.LiveService | querymutatedeploy |
Generated. The messages and service descriptors, and the facade: one typed interface per annotated service, one method per facade call, the binding table the runtime dispatches through, and the Reason union read from the registry. Regenerated by `buf generate` and never hand-edited.
Hand-written, once. The runtime behind those methods: transport, token sources, retry and backoff, the error mapping, the consistency-token store, the pagination iterator and the streams. It contains no method names and no RPC paths.
So adding an SDK method is annotating a service with a module and a method with a facade call, then regenerating — thirteen languages cannot drift, because there is one generator. A service with no module annotation is in no SDK, which is how an admin-only RPC stays out of every one of them.
Without an SDK: curl
Unary RPCs are plain HTTP over the Connect protocol, so no client library is needed to try one. The bodies below are the recorded conformance fixtures, captured from a real local engine.
curl -s http://127.0.0.1:8080/loams.collection.v1.QueryService/Search \
-H 'content-type: application/json' \
-d '{
"namespace": "demo",
"collection": "kb",
"retrievers": [
{"vector": {"field": "embedding", "query": [1, 0, 0], "k": 10}},
{"text": {"query": {"match": {"field": "body", "text": "refund"}}, "k": 10}}
],
"fusion": {"rrf": {"k": 60}},
"limit": 3
}'curl -s http://127.0.0.1:8080/loams.instance.v1.InstanceService/GetInstance \
-H 'content-type: application/json' \
-d '{}'
# The same call as a GET, which a cache may store:
curl -s -G http://127.0.0.1:8080/loams.instance.v1.InstanceService/GetInstance \
--data-urlencode 'encoding=json' --data-urlencode 'message={}'curl -s http://127.0.0.1:8080/loams.instance.v1.InstanceService/WhoAmI \
-H 'content-type: application/json' \
-d '{}'
# With authentication off:
# { "principal": { "id": "dev-loopback", "displayName": "Local developer" } }
# With LOAMS_AUTH_MODE=apikey, the key is read from the environment:
curl -s http://127.0.0.1:8080/loams.instance.v1.InstanceService/WhoAmI \
-H 'content-type: application/json' \
-H "authorization: Bearer $LOAMS_API_KEY" \
-d '{}'curl -s http://127.0.0.1:8080/loams.live.v1.LiveService/Query \
-H 'content-type: application/json' \
-d '{}'
# 501, and the ErrorInfo decodes to:
# reason: "feature_not_in_variant"
# metadata: { "variant": "standard" }# Health, over the Connect JSON mapping:
curl -s http://127.0.0.1:8080/grpc.health.v1.Health/Check \
-H 'content-type: application/json' \
-d '{"service":"loams.instance.v1.InstanceService"}'
# { "status": "SERVING" }
# Reflection, over real gRPC. Mounted in `loams dev`, off elsewhere:
grpcurl -plaintext 127.0.0.1:8080 list
grpcurl -plaintext 127.0.0.1:8080 describe loams.instance.v1.InstanceServiceRun the Rust sample.
Compiled to WebAssembly, fetched only when you press Run.
The sample below is Rust, and it does the one job this page can honestly show running: it reads sample ErrorInfo records and classifies each one against the registry — the same 52 rows printed above, the same ones the TypeScript SDK compiles its Reason union from. Press Run and the result, the per-record verdict and the raw JSON report appear beside the sample data.
Where the compilation happens. Not in your browser. The Rust is compiled to wasm32-unknown-unknown by the repository’s build script, scripts/build-playground-wasm.sh, and the resulting 56 KB module is checked in and served from /wasm/loam-reasons.wasm. There is no Rust toolchain that runs in a browser, so compiling on the click is not something this page can honestly offer; what the click does is fetch the module, instantiate it and execute the compiled program on your sample data. The source shown in the panel is the exact file that was compiled — the build script installs it from the same path.
The sandbox boundary. The module runs inside your browser’s own WebAssembly runtime, which is the sandbox: it has no network, no filesystem, no environment variables and no access to this site’s origin beyond the one fetch of its own bytes. It is instantiated with an empty import object, so it cannot call out to anything at all. There is no server-side execution path here, no code from this page or from you is sent anywhere, and no credential is in the module or in the sample data. What it reads is the registry compiled into it, and what you type.
What can go wrong, and what you see. A fetch or instantiation failure says so and names the stage. A Rust panic! traps the module, and the message the Rust side recorded is shown rather than a blank panel — press Run the panic sample to see that path for real rather than take our word for it. A trap leaves the WebAssembly instance usable, so a panic does not poison the next run.
Nothing has run yet. Press Run and this panel shows what the compiled Rust program made of the sample data: the registry’s own invariants, then each record classified into its Connect code and its error class.