Skip to content
Strata is pre-1.0. APIs, protocol, and storage format may change.

Operations

strata_server reads environment variables at startup. The default configuration is for local development.

  • STRATA_TOKEN (default: dev-strata-token) sets the shared secret. Applications send this value as ?token=. Set a different value for every deployment.
  • STRATA_DATA_DIR (default: data) sets the directory for DETS persistence.
  • STRATA_STATIC_DIR (default: unset) sets the root for static HTTP assets. just example sets this variable. When it is unset, requests outside /admin and the WebSocket path return HTTP 404.
  • STRATA_ADMIN_DIR (default: priv/admin) sets the directory for the built admin page.
  • STRATA_RATE_LIMIT_MESSAGES_PER_SECOND (default: 20) sets the sustained number of decoded non-join messages for each socket. 0 removes the rate limit.
  • STRATA_RATE_LIMIT_MESSAGES_BURST (default: 20) sets the burst bucket capacity (not an extra allowance). 0 uses the sustained rate.
  • STRATA_RATE_LIMIT_JOINS_PER_SECOND (default: 10) sets the sustained join rate per socket. 0 removes the join limit.
  • STRATA_RATE_LIMIT_JOINS_BURST (default: 20) sets the join bucket capacity. 0 uses the sustained join rate.
  • STRATA_ALLOWED_ORIGINS (default: unset) sets a comma-separated list of exact WebSocket origins. An empty list allows every origin.
  • STRATA_MAX_DOCUMENT_BYTES (default: 16777216, or 16 MiB) limits growth of each encoded document, including causal metadata. 0 disables this budget. A negative value fails store startup.

Environment variables do not set the listen port or WebSocket path. An embedded server can change port and websocket_path in StrataConfig.

Run one Strata server against each STRATA_DATA_DIR. One store actor owns the instance's documents, deletion links, and tombstones. Other instances do not replicate or merge that state. Do not point multiple servers at the same data directory, including on a shared volume.

Route all clients that share documents to the same instance. Per-client sticky sessions are insufficient if two collaborators reach different instances. A reconnect must return to the instance that owns the data. Strata does not offer automatic cross-node failover, partition recovery, or zero-downtime rolling upgrades. For a planned replacement, stop writers, complete shutdown, and start the replacement with the same stored data only after the previous owner has stopped.

You can partition independent workspaces across independent deployments through application routing. Give each deployment its own data directory. Keep a parent and its owned descendants together because create and delete require one store owner. References to another instance do not provide cross-instance sync.

One WebSocket can join multiple documents. An upstream doc_id query parameter does not make Strata route each join to another server. If your application selects an instance by workspace or document group, all joins on that connection must belong there. The application must use separate connections for other instances and arrange data migration before changing the routing assignment. This is application-managed partitioning, not the cluster support deferred in #6.

PathPurpose
/socket/websocket?token=…Application WebSocket endpoint
/admin?token=…Read-only admin page
/*Static assets from STRATA_STATIC_DIR, if set
  1. Set STRATA_TOKEN to a strong shared secret. An application with this token can read and write every document. The token also gives access to the admin page.
  2. Set STRATA_ALLOWED_ORIGINS to the exact origins that can open a WebSocket. Separate multiple origins with commas.
  3. Set STRATA_DATA_DIR to a persistent directory. Back up this directory. The storage format can change before 1.0.
  4. Set STRATA_STATIC_DIR only if Strata must serve application assets.
  5. Set STRATA_ADMIN_DIR to the compiled admin directory. Protect the token because the admin page uses the same shared secret.
  6. Set message and join rate limits for the expected workload.
  7. Put a reverse proxy in front of Strata. Configure TLS and per-IP rate limits at the proxy.

Strata passes these settings to beryl's separate per-socket message and join buckets. The existing message defaults remain 20/20; join defaults are 10/20. An excess join receives rate_limited. Beryl drops excess normal messages. These limits do not limit raw undecodable frames. Strata does not apply a limit for each IP address.

See Security Model for token and origin limits. See Durability and Recovery for backup instructions.

Strata has no health endpoint, HTTP metrics endpoint, or structured log format. Monitor the process and the listening port. Keep enough free disk space for the DETS files. Test backup and restore procedures before you depend on the stored data.

An invalid numeric rate-limit value uses the default. Check startup configuration before you expose the server.

The store checks the encoded merged state before accepting apply, create, or delete. If a child or parent exceeds its budget, the entire operation is rejected with document_limit_exceeded; no partial child, parent, or tombstone change is adopted or published. This also applies to correlated requests. No extra encoding pass is needed for admission.

Recovery does not delete documents that exceed a newly lowered budget. Each such document may remain the same size or shrink, but may not grow. Once below the configured budget, the normal limit applies. Zero restores unlimited admission for deployments that explicitly accept that risk.

Use document byte_size in admin status to detect approaching limits. This budget includes user values and retained metadata, not just visible keys. It does not cap total document count, total store memory, transient decode allocations, or permanent document-ID tombstones. The wire frame limit is separate. See Metadata Growth for the v1 limit and migration options.

Each join and reconnect transfers a full document in one text frame. Large documents increase serialization, transfer, and client decode/merge work. Concurrent joins repeat that work. Strata has no chunk threshold or partial-snapshot resume; see protocol limits.

Choose STRATA_MAX_DOCUMENT_BYTES below the lowest message-size limit in your deployed transport path, with room for the document ID and JSON envelope. The default 16 MiB budget is not a tested capacity promise for your proxy, browser, or Erlang client. It also does not bound multi-document create frames, which contain both child and parent state.

Use admin document byte_size as a state-size signal, not the complete frame size. Test full joins and reconnects with representative documents, retained metadata, clients, and concurrent connections through your actual proxy. If a large snapshot fails, inspect client errors and proxy limits before repeated retries. Lowering the budget prevents new growth but does not shrink existing documents. Plan smaller application documents or an explicit document replacement where needed.

store.operational_data and the operational field on read-only admin_status frames expose bounded measurements. The existing admin page ignores this additional field. No admin mutation or new authorization path is added.

MeasurementBoundary
queue_usEnqueue timestamp to the start of the owner handler. Periodic flush uses its scheduled due time.
service_usStart of the owner handler through completion and reply dispatch.
round_trip_usPublic store call entry to response receipt or timeout, including caller scheduling. Periodic flush has no caller round trip.
admin_collectionFull status collection, including summaries, counts, presence reads, operational-data reads, and JSON construction. Its queue is zero; it runs outside the store owner.
deleted_documentsNumber of live documents removed by a successful delete. A repeated tombstone delete records zero.

Snapshot, apply, create, delete, summary, flush, and close have separate statistics. Counts distinguish owner success/failure from caller timeout. A timeout can later be followed by successful owner completion. An accepted mutation count is not proof that a checkpoint contains it.

Checkpoint progress records completed attempts, successes, failures, the accepted-mutation count included by the last successful checkpoint, and the last completion time/result. Flush and close service distributions measure persistence cost. This is checkpoint completion progress, not byte-by-byte DETS write progress or an fsync guarantee.

Counts and extrema cover the current owner lifetime. Each distribution keeps only its latest 128 samples; p50/p95/p99 describe that window, not the whole deployment. Measurements retain no document IDs or payloads and reset after owner replacement. Monotonic microsecond timestamps are local to the BEAM node and are not wall-clock timestamps.

These reads share the store mailbox. A blocked owner can delay or time out a metrics read too; this is not an independent health monitor. Callers send one small follow-up timing message per store call. Recording those samples also costs owner time. Admin snapshots include earlier completed collections, not the collection currently being encoded. Transport serialization and network latency are outside the store timing boundary.

store.close_with_observations returns final data, including successful close latency. Ordinary close keeps its existing result type and discards this snapshot. Forced termination does not promise final measurements.

Run just benchmark-store. It creates an isolated temporary store, seeds 64 documents with 16 KiB values, then runs four concurrent workers for 200 rounds each. Workers mix hot and private document writes, snapshots, two-document subtree deletes, explicit flushes, and full admin collection. The final JSON reports workload parameters, elapsed time, per-class latency distributions, checkpoint progress, and deletion sizes. It includes seed work in the lifetime counts. The temporary store is removed after a successful run.

Repeat the run on the intended BEAM version and storage volume. Record the machine, VM scheduler settings, storage, document sizes, and sample-window limit with results. Change one workload dimension at a time when comparing loads. This local scenario is not a production capacity result.

Keep the global owner while measured tail latency, recovery time, and checkpoint loss windows meet the application's requirements. First compare queue and service distributions by class. Reduce excessive admin polling or checkpoint frequency only if the durability requirement permits it.

Investigate partitioning when repeated representative runs show that slow work for one workload causes another to miss its latency requirement, or when the global owner reaches measured capacity. Compare against a baseline without that slow workload; mailbox length alone is insufficient evidence. Measure the observation cost as part of that comparison.

An ownership-subtree or workspace partition must retain atomic parent-link, child-state, and tombstone changes. A document partition needs an explicit cross-owner protocol for create/delete. Any proposal must also preserve recovery, publication, and rejection semantics. These measurements describe shared work; they do not prove a production bottleneck or justify one actor per document.

strata_server.start(config) returns a running Server. Its RestForOne supervisor starts the store, socket-count tracker, presence, channel runtime, admin collector, and HTTP listener in that order. An abnormal store exit restarts those dependents. The store and tracker use stable named handles; new channel callbacks and the admin collector reach the replacement owner. Existing sockets disconnect and must rejoin for a recovered snapshot.

The supervisor permits two restarts in five seconds. More failures stop the tree. Use an external service manager to restart the application, and inspect the storage error before repeated attempts. PubSub uses a node-local shared scope; it does not own document state or retain a store process reference.

An embedded host must call strata_server.shutdown(server) for an intentional stop. This stops the HTTP listener, admin collector, channels, presence, and tracker before it calls store.close. A successful close ends the supervision tree. A storage error returns StoreCloseFailed; the service stays stopped and the store remains available for a shutdown retry. External callers of document_store(server) must stop writes before calling shutdown.

Supervisor-driven termination also attempts the store's final close, with a 10-second worker shutdown bound. Failure appears in the process error/log path. An untrappable kill, VM crash, or host loss cannot run that close. The default run entry point waits for the supervision tree; it does not install an OS signal handler. Hosts that need a confirmed close on SIGTERM must connect their signal lifecycle to shutdown, not assume a signal proves a flush.