@serve.zone/corestore
Corestore is the node-local storage provider for serve.zone workloads. One process starts a NoSQLDB database endpoint, one ObjectStorage S3-compatible endpoint per configured object-storage pool, a Coreflow-facing control API, and a Docker VolumeDriver plugin. Its storage API reconciles backend-neutral named filesystem and object-storage bindings.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
What It Provides
Corestore packages the storage primitives that a workload usually needs on a serve.zone node:
- Database:
@lossless.org/nosqldb10.7.2 provides authoritative allocation fencing, allocation-bound read grants, restricted persistent views, collection validators and partial indexes. NoSQLDB is the database server and exposes the MongoDB wire protocol on port27017. - Object storage:
@lossless.org/objectstorageexposes one S3-compatible endpoint per physical pool; the default local pool uses port9000. - Control API: HTTP JSON API on port
3000for named storage reconciliation, service-resource provisioning, metrics, snapshots, restores, and archive object replication. - Contract version: the installed
@serve.zone/interfacesrelease is the contract version. Corestore 32.0.0 requires interfaces 32; no request, response, record or digest input carries a version field, and the control paths carry no version segment. - Credential bindings: Interfaces 32 value-free database and object-storage requests are reconciled under publication grants; plaintext is derived only for authenticated, non-cacheable material delivery.
- Docker volumes: Docker's VolumeDriver API over
/run/docker/plugins/corestore.sock. - Backups:
@serve.zone/containerarchivestores deduplicated volume snapshots under the Corestore data directory.
Corestore is deliberately node-local. Volume data lives on the node where Docker mounts it, and the Docker driver reports Scope: local.
Moving To NoSQLDB And ObjectStorage
Corestore uses the published @lossless.org/nosqldb 10.7.2 and @lossless.org/objectstorage 10.7.1 APIs. NoSQLDB 10 reads supported existing stores without a startup or read-time format rewrite. Actual writes can introduce its certified document-ordinal records; after those writes, reopening requires NoSQLDB 10 or later. Preserve a pre-upgrade backup when a binary downgrade is required. Startup accepts only the owning engine's supported current format; older, mixed, incomplete allocation or unsupported roots remain rejected. Authentication is stored in the native database; the removed auth.usersPath option is no longer passed and no legacy user file is imported or deleted. This upgrade does not convert unsupported data automatically. Follow the owning NoSQLDB migration and admission documentation before deploying against existing storage.
NoSQLDB 10.7.2 opens a root that Corestore 33.0.0 (NoSQLDB 10.5.0) wrote in place, without a conversion. The move is one-way for that root as soon as any tenant creates a partial index or a collection with validation options: NoSQLDB up to 10.5.1 refuses a root that holds a partial index and up to 10.6.1 one that holds collection options, so Corestore 33.0.0 no longer starts on it. Keep a pre-upgrade backup if a downgrade must stay possible.
ObjectStorage 10 requires GNU/Linux amd64 or arm64, glibc 2.34 or later, the matching GNU loader, libm.so.6 and libgcc_s.so.1. Musl-only hosts are unsupported. The service image uses the GNU ht-docker-node:lts base. Existing deployments require a separately qualified image rollout and any explicit database format conversion before their persisted runtime image identity is changed.
Both image stages pin the qualified ht-docker-node 7.0.0 LTS digest. Its native package-manager launcher selects this project's exact pnpm 12.5.1 pin. Release tooling uses tsdocker 3.8.1 and tstest 6.3.0; configured image tests start the compiled Corestore provider and verify database persistence across restart.
Provider values smartdb and smartstorage, versioned smartdb.* and smartstorage.* formats, hash domains, export item types, retention metadata, existing data directories and the operator migration command retain their stored identities. Do not rename those values or remove private metadata to force startup. The ObjectStorage migration documentation preserves the corresponding storage and cluster protocols.
Earlier SmartDB 5 Admission Boundary
SmartDB 5 reserves the physical system.views namespace. Before upgrading an existing Corestore installation, inspect every managed database while the older Corestore/SmartDB release is still running and rename or remove any unrelated or noncanonical physical system.views collection. Corestore 6 startup fails closed rather than interpreting such data as a view catalog. This is a compatibility boundary; do not bypass the validation or delete unreviewed database contents during the upgrade.
Workload Database Queries
Corestore embeds NoSQLDB 10.7.2 and persists its own control records with the @lossless.org/client 1.7.0 NoSQL client (@lossless.org/client/nosqldb). Workloads can use the same public client APIs for declared index hints, bounded counts, sorted offset pages, and owned transactions. Hinted reads and counts inside a transaction use its snapshot plus buffered writes. Sparse hints intentionally omit documents absent from the selected index, including when counting an empty filter.
Non-null database fence observations must include the current engine's canonical providerRootId. Corestore validates that identity alongside the exact fence-state schema before continuing a mutation or recovering a durable allocation; missing, malformed or unknown fields fail closed.
NoSQLDB accepts scalar index names and exact ascending/descending key patterns. Missing or ambiguous indexes, $natural, hints on views, and forced multikey traversal fail explicitly. The NoSQL client accepts model-declared index names and _id_; use its finite getOffsetPage() API for pages in an owned transaction. inspectNamespace() provides bounded catalog and document-count observations without creating collections or indexes. Set documentCountMode: 'exact' for unrestricted counts, including collections above 10,000 records; existing work and time budgets still apply. Its result is observational, so callers still need fencing or quiescence when they require a stable namespace.
Workload aggregations run on the engine as sent. NoSQLDB evaluates a composite $group key such as { _id: { owner: '$owner', repo: '$repo' } } per document, so the client's two-field getGroupedTotals() returns one row per distinct pair. A $group key or accumulator operand that uses an operator the engine does not implement, an unsupported accumulator, or a field beside _id that is not a single-accumulator document is refused with TypeMismatch (code 14) before any document is read. A composite key member whose field is absent is reported as null and stays part of the key, where MongoDB omits the member. The owning NoSQLDB aggregation documentation lists the supported operators and accumulators.
A workload owns its schema. create and collMod accept MongoDB's validator, validationLevel and validationAction, and every insert and update is validated; createIndexes accepts a partialFilterExpression, so a unique partial index enforces uniqueness among its members only. $expr evaluates the comparison, arithmetic, conditional, type, array and date expressions a workload sends, $elemMatch takes both its query-operator and its document form, and a transaction statement that meets another transaction's write fails with WriteConflict (112) and TransientTransactionError at that statement, as on MongoDB. The owning NoSQLDB documentation states the exact subsets.
Service Database Roles
Every service database user Corestore provisions, allocates, rotates or binds holds readWrite and dbAdmin on its own tenant database and nothing on any other; the package root exports the set as coreStoreServiceDatabaseRoles. readWrite covers documents and collection DDL, including create with a validator and partial indexes. dbAdmin adds collMod, bypassDocumentValidation and dropDatabase on that database.
Corestore 33.0.0 and earlier granted readWrite alone. The one-time migration step service-database-roles moves every such manifest tenant to the current set through the same fenced, durable ensure provisioning uses, and changes neither its database, user nor password. It runs at start-up in the runtime phase of the ts_migration runner, after NoSQLDB, the manifest and the database mutation coordinator are up and durable allocations have been resumed, and before credential rotations and bindings reconcile; the migration ledger records it, so later start-ups skip it. A manifest tenant is ensured only when NoSQLDB proves it still holds exactly the earlier set with its manifest password. An active credential binding, an adopted legacy database among them, converges through the fenced ensure its start-up validation already runs, and an ordinary credential rotation that has not reached the provider converges when start-up continues it. Start-up completes every delete whose target matches the manifest entry before the step. A released delete the step still sees is left alone: it is the tenant's last fenced mutation, and nothing re-creates the tenant without a newer fence recorded as a new mutation at the same key. Corestore never leaves an unfinished delete for the step: a delete takes its target from the manifest entry, the database name fixes every member of that target except the allocation reference, and only an allocate or replace changes the reference, which the unfinished delete refuses. One the step sees means the manifest changed outside Corestore; it may sit on a live tenant whose fence NoSQLDB never saw, so it refuses the upgrade. An ensure 33.0.0 left prepared completes under the role set it was prepared with before the tenant converges, and a converged scratch allocation still replays its allocation. A scratch allocation 33.0.0 prepared that NoSQLDB saw is replayed by start-up under the role set it was prepared with, released and published into the manifest before the step, which then converges it. A scratch allocation 33.0.0 prepared but never sent to NoSQLDB has no manifest entry and waits for its caller: the replay re-prepares it under the current role set with its unused fence token and grants the current set directly.
The step refuses the upgrade by name before it writes anything. corestore-migration-service-database-roles-unfinished-mutation names the services whose database has an unfinished mutation other than a prepared ensure, or an unpublished scratch allocation NoSQLDB saw but holds no active allocation for; Upgrading from 33.0.0 lists each cause with its remedy. corestore-migration-service-database-roles-unknown-grant names the services whose tenant NoSQLDB proves holds neither role set with the manifest password: restore the tenant's grant or credential before upgrading.
Runtime Layout
Default data root:
/data/corestore
Inside that root Corestore creates:
| Path | Purpose |
|---|---|
smartdb/ |
NoSQLDB file storage, user data, and the authoritative binding, database-mutation, and ObjectStorage provider-lineage registries Corestore writes through its NoSQL client. |
smartstorage/ |
S3-compatible object data. |
volumes/ |
Docker volume mountpoints. |
volume-archive/ |
ContainerArchive repository for volume snapshots. |
restore-staging/ |
Bounded archive objects staged for isolated restores. |
restore-control-locks/ |
Durable isolated-restore lease and fencing records. |
restore-control-state/ |
Durable isolated-restore state and mutation receipts. |
resource-mutation-locks/ |
Existing per-target local and cross-process exclusion records. |
resource-mutation-state/ |
Existing local coordinator counters. Provider fence lineage and token claims live in the NoSQLDB control models. |
database-export-package-locks/ |
Durable offline-package attempt leases and fencing records. |
database-export-package-state/ |
Fsynced offline-package preparing, completion, and release state. |
database-export-package-artifacts/ |
Private per-attempt repositories, inputs, and retained closures. |
database-export-package-capacity/ |
Durable fixed-slot reservations that bound concurrent and retained package artifacts. |
corestore-manifest.json |
Service-resource, volume, and snapshot manifest. It is not authoritative for named storage bindings. |
corestore-secret.json |
Persisted master secret and derived admin credentials. |
Corestore requires its data root and the package lock, state, artifact, and capacity roots to be real directories owned by the effective process user. It hardens accepted roots to mode 0700 and refuses symlinks, foreign ownership, or paths that change identity during startup validation.
The master secret is generated on first start unless CORESTORE_MASTER_SECRET is provided. Database tenant credentials are derived from the service id; named object-storage credentials are derived independently from each stable binding id and are never persisted in binding records.
Configuration
| Env var | Default | Purpose |
|---|---|---|
CORESTORE_DATA_DIR |
/data/corestore |
Persistent data root. |
CORESTORE_BIND_ADDRESS |
0.0.0.0 |
Bind address for control, S3, and DB endpoints. |
CORESTORE_PUBLIC_HOST |
corestore |
The root's identity (fence scope, pool catalog) and the host of the credentials Corestore provisions for itself (/resources/provision, ordinary credentials, read grants), which only the host-local Docker network resolves. Credential bindings never use it: their database values are addressed to the endpoint the binding request states. Must be a canonical IPv4 literal, IPv6 literal without brackets, or lowercase DNS name. |
CORESTORE_CONTROL_PORT |
3000 |
Control API port. |
CORESTORE_S3_PORT |
9000 |
S3 endpoint port. |
CORESTORE_OBJECT_STORAGE_POOLS_JSON |
unset | Strict JSON for additional local or host-mounted NFS object-storage pools. |
CORESTORE_DB_PORT |
27017 |
NoSQLDB MongoDB-wire endpoint port. |
CORESTORE_REGION |
us-east-1 |
Region value for S3 credentials. |
CORESTORE_API_TOKEN |
unset | Required 32–4096 byte control token; configure this or CORESTORE_API_TOKEN_FILE, never both. |
CORESTORE_API_TOKEN_FILE |
unset | Alternative file containing the control token, with one optional trailing newline. |
CORESTORE_RESTORE_GRANT_KEYRING_FILE |
unset | JSON keyring containing trusted RSA public keys for isolated-restore grants. |
CORESTORE_RESTORE_GRANT_ISSUER |
unset | Exact expected iss claim for isolated-restore grants. |
CORESTORE_RESTORE_GRANT_AUDIENCE |
unset | Exact expected aud claim for isolated-restore grants. |
CORESTORE_CLUSTER_ID |
unset | Cluster identity to which isolated-restore grants must be bound. |
CORESTORE_NODE_NAME |
unset | Node identity to which isolated-restore grants must be bound. |
CORESTORE_ISOLATED_RESTORE_LEASE_MS |
3600000 |
Durable restore/resource mutation lease duration in milliseconds. |
CORESTORE_DATABASE_EXPORT_PACKAGE_LEASE_MS |
3600000 |
Durable offline-package attempt lease duration in milliseconds. |
CORESTORE_DATABASE_EXPORT_PACKAGE_MAX_RETAINED_BYTES |
1073741824 |
Aggregate package admission ceiling; it must be a positive safe integer yielding between 1 and 4,096 full 256 MiB slots (minimum 268435456). A remainder below one full slot is discarded. |
CORESTORE_MASTER_SECRET |
generated and persisted | Seed for deterministic tenant credentials. |
CORESTORE_DB_ROOT_USER |
corestore_root |
NoSQLDB root username. |
CORESTORE_DB_ROOT_PASSWORD |
derived or persisted | NoSQLDB root password override. |
CORESTORE_S3_ADMIN_ACCESS_KEY_ID |
derived or persisted | ObjectStorage admin access key override. |
CORESTORE_S3_ADMIN_SECRET_ACCESS_KEY |
derived or persisted | ObjectStorage admin secret override. |
CORESTORE_VOLUME_PLUGIN |
enabled |
disabled never creates the Docker VolumeDriver socket; a host without Docker runs this way. Any other value than enabled or disabled prevents startup. |
CORESTORE_VOLUME_PLUGIN_SOCKET |
/run/docker/plugins/corestore.sock |
Docker VolumeDriver socket path. Setting it while CORESTORE_VOLUME_PLUGIN=disabled prevents startup. |
CORESTORE_ARCHIVE_PASSPHRASE |
unset | Optional ContainerArchive encryption passphrase. |
CORESTORE_VERBOSE |
false |
Enables verbose ObjectStorage logging when set to true. |
Corestore refuses to start unless exactly one control-token source is configured. Tokens must contain no whitespace. The file form must be a regular, non-symlink file and is preferable when the orchestrator can mount secrets. Every TCP control API endpoint except GET /health requires exactly one Authorization: Bearer <token> header; the legacy x-corestore-token header is rejected.
The five isolated-restore settings must be configured together to enable signed restore grants. Partial configuration prevents startup; leaving all five unset keeps ordinary Corestore APIs available but makes isolated-restore operations unavailable. The keyring has this exact shape:
{
"keys": [
{
"kid": "restore-key-1",
"publicPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
]
}
Object-storage pools
The implicit default pool preserves the existing local ObjectStorage identity at <CORESTORE_DATA_DIR>/smartstorage and CORESTORE_S3_PORT. Additional pools are configured by CORESTORE_OBJECT_STORAGE_POOLS_JSON. Each pool has a unique endpoint, physical root, backend identity, and one or more portable policy classes. Corestore starts and reconciles one ObjectStorage provider per pool.
Example host-mounted NFS capacity pool:
{
"pools": [
{
"id": "archive-hdd",
"port": 9001,
"directory": "/storage-pools/archive-hdd",
"backend": {
"kind": "mountedFs",
"expectedFilesystemType": "nfs",
"expectedSource": "192.0.2.10:/volume1/onebox"
},
"classes": [
{
"classId": "onebox.object-storage.capacity.v1",
"revision": "1",
"kind": "objectStorage",
"performanceTiers": ["capacity"],
"durabilities": ["persistent"],
"topologies": ["singleNode"],
"hardQuota": false,
"snapshotModes": ["none"],
"backup": false,
"encryptedInTransit": false,
"accessModes": ["readWrite"],
"versioning": false,
"retention": true
}
]
}
]
}
The host must mount NFS before Corestore starts and bind-mount the exact mount root into the Corestore container at the configured directory. Corestore does not mount remote filesystems itself. Mounted pools require an exact numeric IPv4 server:/absolute/export source and the Linux nosymfollow VFS option. NFS server-coordinated locking must remain enabled: nolock and local_lock=all|flock are rejected, while an absent local_lock option, local_lock=none, or local_lock=posix is accepted. ObjectStorage validates the mount type, source, mountpoint, remote locking, symlink policy, and publication primitives before accepting traffic. A missing, replaced, or mismatched mount fails startup instead of writing into the underlying local directory.
Pool ids, ports, directories, class revisions, and backend identities are persisted as an immutable catalog. Once a pool is adopted, its definition cannot be changed or removed, even when no live binding currently refers to it; adding a class revision to that existing pool also changes its immutable identity and is rejected. Add a new pool containing the new class revision and use the fenced migration protocol when physical placement must change.
Starting Corestore
Install the published package in a consumer:
pnpm add @serve.zone/corestore @serve.zone/interfaces@32.41.0
pnpm exec corestore
For a repository checkout:
pnpm install
pnpm build
pnpm start
For TypeScript development:
pnpm run startTs
Programmatic startup:
import { CoreStore } from '@serve.zone/corestore';
const corestore = new CoreStore({
dataDir: '/var/lib/serve.zone/corestore',
apiTokenFile: '/run/secrets/corestore-api-token',
restoreGrantKeyringFile: '/run/secrets/corestore-restore-keyring.json',
restoreGrantIssuer: 'https://cloudly.example',
restoreGrantAudience: 'corestore.restore',
clusterId: 'cluster-1',
nodeName: 'node-1',
});
await corestore.start();
Prepared filesystem volume adoption
adoption.js is the private offline owner entrypoint for one
adoptPreparedFleetCutoverVolume request from Pallet. Pallet must already hold the exact durable
TFleetCutoverVolumeFence and its per-volume native mutation lane. The old Corestore and every
legacy writer must remain stopped for the whole invocation. Put the request object, with exactly
handoffIntent and palletFence, in a bounded mode-0600 regular JSON file, then run:
CORESTORE_DATA_DIR=/data/corestore \
CORESTORE_STORAGE_ADOPTION_REQUEST_PATH=/run/corestore/adoption-request.json \
node adoption.js
CORESTORE_DB_ROOT_USER may name the existing control-store root user; it defaults to
corestore_root. The password is read only from the protected corestore-secret.json in the data
root.
The command writes only the JSON { "adoption": ..., "claim": ... } response to stdout and sends
diagnostics to stderr. It starts no Corestore service, HTTP endpoint, S3 provider, object-storage
plugin, or callback. It opens the local control database through a private temporary Unix socket
with fileStorageStartup: "existing-only", writes the ready binding, private physical lineage,
exclusive volume/claim owner, full claim and public adoption receipt in one transaction, closes
every handle, and removes the socket.
The data root must be the canonical mode-0700 root owned by the effective user. Its protected
manifest and secret must be ordinary private files, the manifest mount must select exactly one
existing unmounted volume, and that volume must contain at least one nonempty regular-file payload;
an empty directory, directories only, or zero-length files only cannot be adopted. smartdb/ must
already be a committed current NoSQLDB root. The one-shot never converts a legacy database engine
and never creates an empty control store. Complete and verify the documented whole-owner engine
conversion, including its authenticated old-source envelope preflight, before filesystem adoption.
A missing, old, mixed, empty, or still-open root is rejected.
An exact request replay returns the durable original claim and receipt after rechecking the same manifest and physical volume. A changed handoff, fence, claim, manifest, physical lineage, or a second owner for the volume fails closed. Normal Docker VolumeDriver create, mount, unmount and remove operations refuse an adopted volume; Pallet remains the owner of later native attachment transitions and of consuming the held fence.
Offline MongoDB-wire migration
The image includes one explicit operator migration that copies a stopped workload's database from a MongoDB-wire-compatible source into Corestore's NoSQLDB backend. The migration is not part of normal startup. First provision the target through Corestore's normal database resource API while Corestore is running, but do not deliver its credentials to the workload. Then stop Corestore and fence every source writer. The migration requires that exact NoSQLDB tenant and its already-durable manifest entry, reads the manifest without rewriting it, replaces an empty target or resumes an exact replay behind NoSQLDB's durable publication hold, releases publication, and verifies canonical BSON documents and complete index definitions. The source database may contain no collections. The migration persists through Corestore's normal database-mutation coordinator, so provision, migration, later snapshot/restore, and deprovision remain in one durable fence lineage. An exact replay with the same migration id and fencing token returns the same released result.
The migration rejects collection options, including validators, and index kinds or options it does not carry, including partial indexes. It also fails closed above a 96 MiB serialized export, 10,000 collections, 1,000,000 documents, or 100,000 indexes. Fix and release a missing NoSQLDB capability before retrying; split a larger migration through an explicitly reviewed process instead of weakening this preflight.
If the running Corestore uses CORESTORE_CLUSTER_ID and CORESTORE_NODE_NAME, the offline migration requires those same values so its NoSQLDB fence remains in the existing resource-mutation scope. Configure both or neither.
CORESTORE_MIGRATION_SOURCE_FENCED=true \
CORESTORE_MIGRATION_CORESTORE_STOPPED=true \
CORESTORE_MIGRATION_TARGET_CREDENTIALS_UNDELIVERED=true \
CORESTORE_MIGRATION_SOURCE_MONGODB_URI='<protected MongoDB URI>' \
CORESTORE_MIGRATION_SOURCE_DATABASE='<source database>' \
CORESTORE_MIGRATION_SERVICE_ID='<stable service id>' \
CORESTORE_MIGRATION_SERVICE_NAME='<service name>' \
CORESTORE_MIGRATION_ID='<stable migration id>' \
CORESTORE_MIGRATION_FENCING_TOKEN='<positive integer>' \
pnpm run migrate:legacy-mongodb-to-smartdb
CORESTORE_DATA_DIR, CORESTORE_PUBLIC_HOST, and CORESTORE_DB_PORT select the target Corestore instance and keep their normal defaults. Never place the source URI in logs or committed configuration.
Whole-owner SmartDB 5 to NoSQLDB 10 conversion
migration.js also supports a versioned whole-owner engine conversion for a Corestore data root written by SmartDB 5. It is an offline operator operation, never an automatic startup fallback. engine-source-inventory reads the protected manifest and MongoDB-wire namespace list and reports only owned database names and allocation status. It requires an exact match between corestore_control, every manifest-owned database, and the wire inventory; an unowned database or an admin collection refuses conversion. engine-source-export exports the control database and every ordinary tenant through bounded read-only wire operations into a new mode-0600 artifact. It preserves canonical BSON numeric widths and complete supported index specifications, including TTL seconds. A preliminary export with CORESTORE_ENGINE_SOURCE_FENCED=false is for rehearsal only.
For an authoritative export, stop all workload writers, establish the orchestrator's source fence, and set CORESTORE_ENGINE_SOURCE_FENCED=true. Keep the old Corestore wire endpoint available for that export, then stop Corestore. Preserve a separate verified checkpoint. Copy the complete non-database Corestore data root to an isolated stage, including corestore-manifest.json, corestore-secret.json, object storage, volumes, and coordinator state; leave smartdb absent in the stage. The export artifact and staged root contain private data. Do not deliver target credentials or activate the target during conversion. The stage command refuses any allocation-managed database; finish its owning operation or use its exact supported deprovision/transfer contract first.
Generic and preliminary wire exports retain a database-wide snapshot transaction. Only an
authoritative whole-owner export with both CORESTORE_ENGINE_SOURCE_FENCED=true and a validated
source digest proof uses bounded nontransactional pages. SmartDB 5 intentionally caps the bytes and
documents materialized by one find, so the converter retries only that exact capacity refusal with a
strictly smaller page and discards the complete failed page attempt. It never falls back from a
snapshot error. The fenced page order is valid only while the old physical engine is immutable; the
converter requires equal collection counts before and after each collection, complete page totals,
an unchanged collection/index catalog, and exact owner-digest totals. The orchestrator must still
prove the authenticated old-owner digest is identical before and after export.
pnpm run migrate:engine-source-inventory
pnpm run migrate:engine-source-export
pnpm run migrate:engine-stage
pnpm run migrate:engine-activate
Inventory and export require CORESTORE_ENGINE_SOURCE_MANIFEST_PATH and CORESTORE_ENGINE_SOURCE_SECRET_PATH in the same read-only mounted source root, plus CORESTORE_ENGINE_SOURCE_HOST and CORESTORE_ENGINE_SOURCE_DB_PORT. Set CORESTORE_ENGINE_SOURCE_DB_ROOT_USER only when the source used a non-default root user. The converter derives the protected wire URI in memory; never pass credentials through Docker environment or command arguments. Export additionally requires an absolute, initially nonexistent CORESTORE_ENGINE_EXPORT_PATH and explicit CORESTORE_ENGINE_SOURCE_FENCED=true or false. Stage reads that private artifact and requires absolute CORESTORE_ENGINE_STAGE_DATA_DIR and initially nonexistent CORESTORE_ENGINE_STAGE_RECEIPT_PATH, stable CORESTORE_ENGINE_MIGRATION_ID, CORESTORE_PUBLIC_HOST, CORESTORE_DB_PORT, CORESTORE_S3_PORT, CORESTORE_REGION, and all three assertions CORESTORE_ENGINE_SOURCE_FENCED=true, CORESTORE_ENGINE_CORESTORE_STOPPED=true, and CORESTORE_ENGINE_TARGET_CREDENTIALS_UNDELIVERED=true. Supply the runtime's exact CORESTORE_CLUSTER_ID/CORESTORE_NODE_NAME pair and CORESTORE_OBJECT_STORAGE_POOLS_JSON when configured.
SmartDB 5 can expose an empty physical system.views catalog while refusing wire listIndexes and reads on it. The converter preserves that namespace and its canonical _id_ index only with an orchestrator-owned source digest proof. Set CORESTORE_ENGINE_SOURCE_DIGEST_PROOF_PATH to the private, exact mode-0600 proof file and CORESTORE_ENGINE_MIGRATION_ID to its migration identity for engine-source-export. The proof has format onebox.corestore-engine.source-digest-proof.v1, binds the checkpoint, source container/image, manifest hash and database names, and contains authenticated old-owner smartdb.database.content-digest.v1 entries for every ordinary tenant. The orchestrator must obtain those entries through the fenced old Corestore owner's digest endpoint, journal the exact proof bytes and SHA-256 before export, then repeat the digest reads and require equality afterward. Corestore verifies the proof's shape and exact inventory and independently checks collection, document and index totals for every proved ordinary tenant, including one without system.views; it cannot authenticate proof authorship by itself. A missing, nonempty, changed or uncertain catalog refuses conversion. The private export artifact retains sourceDigestProof: { proof, sha256 }; its value-free stdout summary reports sourceDigestProofSha256 for the orchestrator's journal check.
The old engine stores the catalog _id_ index as {name,key} but its wire listCollections.idIndex reports {v:2,key,name}. The converter uses that canonical wire metadata and compares the complete transformed collection set against the held target readback. It does not compare old-owner and target content-digest SHA values across this metadata normalization; the orchestrator instead requires identical authenticated old-owner digests before and after export.
Stage imports corestore_control into a fresh NoSQLDB root, verifies it, runs the ordered Corestore control-data migrations, then ensures every ordinary tenant and imports it through Corestore's durable database-mutation coordinator. It independently reads each tenant through the exact held-publication capability and compares canonical BSON documents and index definitions with the fenced source. The stage command writes each value-free held receipt and digest into the new private mode-0600 receipt file and returns its SHA-256 in the stdout summary; the orchestrator durably journals that hash before proceeding. Stage refuses any control-database TTL index because control migrations require its data plane while tenant holds remain closed. A failed or interrupted stage is discarded in full and recreated from the checkpoint and fenced export; never activate a partial stage.
engine-activate requires the same staged root, fenced export, migration identity, runtime endpoint and cluster identity, plus CORESTORE_ENGINE_STAGE_RECEIPT_PATH pointing at that exact private result and CORESTORE_ENGINE_ACTIVATE=true. It reopens the same physical NoSQLDB root, checks the migrated control digest and independently re-exports every still-held tenant before releasing any tenant. Released tenants from an interrupted activation must match their durable held-receipt hash; exact retry resumes the remaining holds. Activation is the deliberate publication step; only then may the orchestrator start the isolated Corestore runtime, qualify native tenant ownership and object access, and reconcile resource consumers. The converter does not copy object bytes itself or rotate previously delivered credentials.
Each database export is limited to 96 MiB, 10,000 collections, 1,000,000 documents, and 100,000 indexes; the whole owner is limited to 256 MiB of serialized database export. Collection options, index kinds or options that the target engine cannot enforce, active allocations, or larger owners require an owning migration capability before conversion. These refusals never authorize dropping data or weakening equality checks.
Converting a Corestore 3.0.0 root (SmartDB 2.14.8)
A Corestore 3.0.0 root has SmartDB 2.14.8 under smartdb/, SmartStorage 8.0.4 under smartstorage/ (the byte layout ObjectStorage 10.3.1 reads unchanged, including .smartstorage/retention and every object's .retention.json), a manifest with version: 1, lease files with version, the 3.0.0 pool catalog, and credential-slot, secret-bundle and schema-2 storage-binding collections. The same inventory, export, stage and activate operations convert it; stage runs the control-data migrations that rewrite all of that. Legacy resources remain in the manifest and move to credential bindings through an authorized adoption (see Runtime credential bindings).
SmartDB 2.14.8 differs from SmartDB 5 in three ways the procedure has to absorb:
- No owner digest endpoint. 2.14.8 cannot supply the source digest proof, so an export with
CORESTORE_ENGINE_SOURCE_FENCED=truetakes a database-wide snapshot per database instead. The proof that nothing wrote during the export is a second fenced export:pnpm run migrate:engine-source-export-comparewithCORESTORE_ENGINE_EXPORT_PATHandCORESTORE_ENGINE_COMPARE_EXPORT_PATHreports each database's collection-set SHA-256 from both private artifacts and exits non-zero unless both are fenced, cover the same inventory and manifest, and every collection set is identical. Stage only the first artifact, and only after the comparison answersidentical. - Anti-clone identity. 2.14.8 binds its provider identity and resource-fence registry to the storage root's path, device and inode, and refuses any copied root with
EFENCE_STATE_CORRUPT. Export either from the original engine while its writers are fenced, or from a disposable copy whosesmartdb/.__rustdb_internal/provider-identity.jsonandsmartdb/.__rustdb_internal/resource-fencing/were removed. The conversion never reads that registry; the new NoSQLDB root establishes its own fences. Never remove them from the original root. - Identity-bound roots. Stage and activate need the old root's
CORESTORE_PUBLIC_HOSTandCORESTORE_DB_PORT(the manifest's credentials embed them), and the converted runtime needs the same data path (the pool catalog records it,/data/corestorein the image) and the sameCORESTORE_S3_PORT. LeaveCORESTORE_CLUSTER_IDandCORESTORE_NODE_NAMEunset when 3.0.0 ran without them: the runtime then reproduces 3.0.0's fence scope (clusterIdlocal,targetNodeName= public host), and an adopted bucket continues in that lineage.
Copy smartstorage/ byte for byte (cp -a or tar preserving modes); only a byte-exact copy keeps every retain-until. Verify it before and after conversion with a tree hash, for example (cd smartstorage && find . -type f -print0 | sort -z | xargs -0 sha256sum) | sha256sum, which must be identical for the source copy and the staged root. After activation and the first start, compare every legacy bucket through S3 as well: take the listing of each bucket from the source with the legacy credential before conversion, and after adoption compare it with GET /control/credential-bindings/<id>/object-listing of the adopting binding, which needs no credential beyond the control token. It answers { "listing": { "bindingId", "bucketName", "objectCount", "totalBytes", "listingSha256" } } (ICoreStoreCredentialBindingObjectListing) and nothing else — no key, no object data, no credential. listingSha256 is the lowercase hex SHA-256 over, for every object in ascending UTF-8 byte order of its key, JSON.stringify([key, size, etag]) followed by a newline, size the byte length as a number and etag the ETag without its surrounding quotes, so the same digest follows from the Key, Size and ETag of an S3 ListObjectsV2 of the source. Take it with the bucket's writers stopped; a binding that is not an active object-storage binding answers 409 credential binding does not own an active bucket. The listing reads the bucket's ListObjectsV2 pages and nothing else, and it is bounded: at most 1,000,000 objects, 5 minutes from admission to the last page, one listing per binding and two at once across all bindings, and it stops when the requesting connection closes. A refused listing answers 503 with code object-listing-busy (this binding is already being listed, or two listings run), object-listing-object-limit, object-listing-time-budget, object-listing-aborted (Corestore stopped) or object-listing-unavailable (object storage is not running or did not answer a valid page).
Upgrading from 33.x
Corestore 34 moves to containerarchive 0.13 (34.0.0 shipped 0.11; both use the index-generation
format introduced in 0.10), which reads only repositories with index generations; 33.x wrote the node's volume archive (<dataDir>/volume-archive) in the legacy
index/ layout of containerarchive 0.3.2. The first start of 34.0.0 converts it in the data
migration step volume-archive-index-generations, before any archive is opened:
- containerarchive copies the legacy repository into
<dataDir>/volume-archive.convertedin the current layout (ContainerArchive.migrate(source, { destination })), leaving the legacy repository byte-identical; - the legacy repository is renamed to
<dataDir>/volume-archive.pre-34, the rollback copy; - the converted copy is renamed to
<dataDir>/volume-archive.
Every step is durable and resumes after a crash. The copy needs the archive's size free on the data
directory's filesystem plus a reserve (64 MiB and 1 % of the copy). Before any migration step runs,
start-up estimates the copy (containerarchive's estimateMigration, by the rule the copy applies)
and, when it does not fit, refuses with corestore-migration-archive-space-insufficient, naming the
bytes to copy, the bytes required and the bytes available: no step has run or been recorded, so
free the difference and start again. The step records itself, so later starts skip it.
The conversion takes time in proportion to the archive, in two phases: the copy of its files, then
the build of the converted archive's index (the legacy index segments read, the new index written
and read back) and the rename. containerarchive reports both phases, the index build once per
segment, so the host executable extends systemd's start timeout while either advances, reports it
as STATUS=, and once the conversion has finished gives the rest of start-up a fresh start budget
(see Start-up progress under Host Service). A resumed conversion reports too: one interrupted
before its index was written compares the files already copied (reported as the copy) and builds
the index again; one interrupted after that compares nothing and reports only its index phase, with
the file counts at 0.
The unit's base TimeoutStartSec still has to cover everything before the conversion's first
progress notification: start-up up to the conversion, including the earlier migration steps; the
free-space estimate and the copy's planning, each of which lists every file of the archive, so this
part grows with the number of files, though not with their size; and up to one 10-second check.
Size the base for that; the copy and the index build themselves need no share of it. A conversion
that stalls stops the extensions and the start fails about 30 seconds after the last one, or at the
base deadline if that is later. A stop (SIGTERM) during the copy ends it before its next file,
and one during the index build before the new index is written, exactly as a crash there would;
the next start resumes it.
Before upgrading, drain the short-lived archive operations with 33.x: every isolated restore
(<dataDir>/restore-staging/<restoreId>/archive), database backup import
(<dataDir>/restore-staging/database-backup/…) and export package
(<dataDir>/database-export-package-artifacts/<attemptId>/repository) must be finished or cleaned
up. While any of them holds a repository, start-up refuses with
corestore-migration-archive-operations-in-flight, naming them, and converts nothing.
No archive is converted as a side effect of opening it; a legacy one is refused with 409. Two
restore paths convert explicitly, through containerarchive's private-copy migration (their
replication leaves locks/repository.lock out, and only this process opens the copy):
- An isolated restore whose staged archive (a backup a 33.x node took) holds the legacy layout
converts it into
<dataDir>/restore-staging/<restoreId>/archive.convertedbefore it executes; the staged repository stays as staged, so its object inventory keeps verifying. Cleanup removes both. - A volume archive
/archive/object/writerestored in the legacy layout is refused untilPOST /archive/convertconverts it; the restored legacy repository is kept as<dataDir>/volume-archive.restored-legacy. While that copy exists, converting a second legacy restore is refused withcorestore-migration-archive-swap-ambiguous; the operator removesvolume-archive.restored-legacyonce satisfied with the converted archive.
volume-archive.pre-34 is kept until an operator removes it. To roll back to 33.x: stop 34.0.0,
remove <dataDir>/volume-archive (the converted archive, including snapshots taken since), rename
volume-archive.pre-34 back to volume-archive, and start the previous release. Snapshots taken on
34.0.0 are lost by that. 34.0.0 keeps NoSQLDB at 10.7.2, and ObjectStorage 10.4.0 changes no storage
format (it adds engine selection only), so the rest of the data directory stays in the formats
33.3.0 reads.
Upgrading from 33.0.0
The first start of 33.1.0 runs one migration step, service-database-roles, in the runtime phase of ts_migration/runner.ts: after NoSQLDB, the manifest and the database mutation coordinator are up, and before credential rotations and bindings reconcile. It moves every service database user from readWrite to readWrite plus dbAdmin through the fenced ensure and records itself in the CoreStoreDataMigration collection, so a second start skips it. Before upgrading, finish every held or prepared database import with 33.0.0. The step refuses start-up by name, as described in Service Database Roles, for these causes:
corestore-migration-service-database-roles-unfinished-mutation, a held or prepared database import: start 33.0.0, which still opens the root, replay the import with its original request (a prepared import continues to held, a held one returns its held receipt), commit the held receipt through/resources/database/import/commit, then upgrade again.corestore-migration-service-database-roles-unfinished-mutation, any other cause: a prepared or held delete whose recorded target does not match the manifest entry, a prepared allocation NoSQLDB never saw beside a manifest entry, or a prior-role scratch allocation without a manifest entry whose fence NoSQLDB saw but for which it holds no active allocation. Corestore does not write these states. Start-up completes every delete whose target matches the manifest entry and every allocation NoSQLDB holds active, and while a mutation is unfinished every other mutation on that database is refused, so the manifest target cannot drift; NoSQLDB commits an allocation and its fence at one position. Replaying the deprovisioning does not help either: 33.0.0 derives the delete from the current manifest entry and refuses it with409 target database has another active mutation. These states mean the manifest, the control database or the NoSQLDB root changed outside Corestore, for example a hand-edited manifest or a partial restore. Restore all three from one consistent point; start-up then completes the delete or publishes the allocation.corestore-migration-service-database-roles-unknown-grant: NoSQLDB proves a manifest database holds neitherreadWritenorreadWriteplusdbAdminwith the manifest password. Restore the tenant's grant or credential, then upgrade again.
The root stays readable by 33.0.0 until a tenant creates a partial index or a collection with validation options.
Upgrading from 7.0.0
This release renames every version-bearing token Corestore owns, so values it already wrote have to
be rewritten before the runtime can read them. Corestore does that itself: the first start of 32.0.0
runs ts_migration/runner.ts after the control database opens and before any coordinator reads its
on-disk state. Every step is idempotent, its completion is recorded in the CoreStoreDataMigration
collection, and a step that meets state it cannot rewrite refuses start-up by name without changing
anything. A second start is a no-op; an interrupted start resumes where it stopped.
What migrates automatically:
| Step | What it rewrites |
|---|---|
storage-binding-fields |
Drops schemaVersion from the record and its resolved binding, renames the storage class id and the object-storage file delivery format, and recomputes requestDigest, specDigest, intentDigest and serviceRequestKey with the runtime's own digest functions. |
credential-binding-fields |
Drops schemaVersion from the record, the binding request and the publication grant, recomputes bindingRequestDigest, and carries it into the grant and into the object-storage retention evidence. That evidence loses the version this release added to its capability evidence, and its intentSha256 is re-derived because the digest input dropped a version member; the provider's own sentinel version is kept verbatim. |
database-mutation-fields |
Drops schemaVersion and renames the portable payload format. It deliberately leaves requestSha256, replaceHistory[].requestSha256 and smartDbFence untouched — see below. |
storage-pool-catalog-fields |
Drops schemaVersion, renames the class keys and adopts the pool's current configDigest, but only while the pool's durable providerIdentity still matches the configuration. |
filesystem-volume-options |
Drops version from corestore-manifest.json and rewrites every Corestore-owned volume's servezone.storage.* options to the migrated binding's digest. |
coordinator-protocol-trees |
Collects the lease trees 7.0.0 wrote. Its claims, release markers, commit receipts and export-package capacity reservations carry a version member this release refuses, and the export-package coordinator reads them on every start, so a data directory that keeps them cannot start at all. |
Preconditions, all of them checked before the first step writes anything:
- Quiesce database mutations. Commit or release every held publication before stopping 7.0.0. A
held publication is a NoSQLDB barrier only the issuing release can clear: its held receipt is
rebuilt from
requestSha256and the payload format, both of which this release states differently. - No isolated restore still owns its staging directory. A restore's coordinator configuration
and resource-lease identities both change, so a staged run cannot be resumed across the upgrade,
and collecting the 7.0.0 lease trees leaves the start-up sweep no state to read. Only a staging
directory whose latest durable state is present and not
cleanedrefuses; the named recovery is to finish that restore under 7.0.0, or to remove the named directory underrestore-staging. Directories the release already cleaned, and the lock directories left behind underresource-mutation-locks, are garbage the migration collects; they are never converted. - No object-storage migration records. A live object-storage migration is a two-party protocol:
its consumer holds
migrationDigest,candidateDigestand the snapshot digests and sends them back. Complete or abort every migration and clear its records first. - 7.0.0 database backup receipts are re-exported, not converted. Their
formatis nowcorestore.database.backup, which the receipt reader does not recognise. Take a fresh export with this release instead; a re-issued receipt for the same backup replays onto the samerestoreId. - Clients request new read grants after the restart. Nothing about a read-access grant is
persisted, NoSQLDB retires every active grant when its engine restarts, and the credentials derive
from a renamed HMAC domain. Ask for a new
grantId; do not replay the old one.
The refusals read as the precondition that was missed, with the records that caused them:
corestore-migration-database-mutation-held-publication: 1 database mutation(s) hold a NoSQLDB
publication that only the issuing release can commit or release; commit or release them before
upgrading (<targetResourceId>)
corestore-migration-isolated-restore-in-flight: an isolated restore is staged by a release whose
coordinator configuration and resource lease identities this upgrade renames; finish the restore
under that release, or remove its staging directory under <dataDir>/restore-staging (<restoreId>)
corestore-migration-object-storage-migration-records-present: 1 object-storage migration record(s)
and 0 migration object record(s) were written by a release whose digests this upgrade renames;
complete or abort every object-storage migration and clear its records before upgrading
corestore-migration-storage-binding-unmigratable,
corestore-migration-credential-binding-unmigratable,
corestore-migration-credential-operation-claim-unmigratable,
corestore-migration-database-mutation-unmigratable and
corestore-migration-filesystem-volume-unowned name a single record that does not validate in its
32 shape after the rewrite; each carries the record's identity and the underlying contract error as
its cause.
Why the database fence is not rewritten: NoSQLDB pins a database's durable fence scope at the
record's current token, and the record reader binds smartDbFence to the provider receipt inside
heldResult / releasedResult. Rewriting the scope would either be refused at the current token or
require forging that receipt. Left as written, the next mutation takes token + 1, and the strictly
newer token carries the new scope. The cost is that replaying a pre-upgrade restoreId answers
409 database backup restore identity does not match durable state, because the history entry keeps
only the digest and not the request it was computed from.
Control API
GET /health is always unauthenticated:
curl http://corestore:3000/health
All other control endpoints require exactly one Authorization: Bearer <token> header.
Useful endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/health/details |
Returns authenticated component-level health details. |
GET |
/metrics |
Returns database, object storage, and volume metrics. |
GET |
/control/storage/capabilities |
Advertises enforced portable storage guarantees and exact policy revisions. |
POST |
/control/credential-bindings/reconcile |
Reconciles one Interfaces 32 value-free credential binding under its exact publication grant. |
GET |
/control/credential-bindings?serviceId=<id> |
Lists secret-free credential binding status, optionally for one service. |
GET |
/control/credential-bindings/<id> |
Returns one secret-free credential binding status. |
POST |
/control/credential-bindings/<id>/material |
Returns grant-bound plaintext material with Cache-Control: no-store and Pragma: no-cache. |
GET |
/control/credential-bindings/<id>/object-listing |
Returns a value-free listing of an active object-storage binding's bucket: object count, total bytes and one digest over every key, size and ETag. |
POST |
/control/credential-bindings/<id>/retain |
Permanently marks an omitted or disabled binding retained without deleting its provider resource. |
GET |
/control/legacy-resources |
Lists every service's legacy /resources/provision resources and their adoption state, value-free (ICorestoreLegacyResourceInventory). |
POST |
/control/ordinary-credentials/rotate |
Rotates one legacy manifest resource under a durable generation and returns a value-free receipt. |
POST |
/control/ordinary-credentials/material |
Returns current, provider-attested resource environment material for an exact active rotation receipt; never cache it. |
POST |
/control/storage/bindings/reconcile |
Idempotently reconciles one stable named binding. |
GET |
/control/storage/bindings?serviceId=<id> |
Lists canonical secret-free bindings for a service. |
GET |
/control/storage/bindings/<id> |
Gets one canonical secret-free binding. |
POST |
/control/storage/bindings/<id>/credentials |
Returns object-storage credential material to the authenticated adapter with Cache-Control: no-store. |
POST |
/control/storage/bindings/<id>/release |
Applies the binding's declared retain/delete policy and persists a tombstone. |
GET |
/control/storage/bindings/<id>/migration-snapshot |
Captures the exact unfenced source binding snapshot required to prepare a migration. |
POST |
/control/storage/migrations/prepare |
Fences the source and prepares a hidden destination binding on a different pool. |
GET |
/control/storage/migrations/<id> |
Returns the durable migration phase and required consumer action. |
POST |
/control/storage/migrations/<id>/quiesce |
Accepts bound quiesce evidence, seals the source, verifies the destination, and performs the durable cutover. |
POST |
/control/storage/migrations/<id>/activation |
Accepts bound evidence that the consumer started on the destination. |
POST |
/control/storage/migrations/<id>/cleanup |
Performs fenced source cleanup after destination activation. |
POST |
/control/storage/migrations/<id>/abort |
Aborts a pre-cutover migration through its exact recovery contract. |
POST |
/control/storage/migrations/<id>/resume |
Resumes a retryable phase without bypassing its durable fence. |
GET |
/volumes |
Lists managed Docker volumes. |
GET |
/volumes/snapshots?name=<volume> |
Lists snapshots for one volume or all volumes. |
POST |
/volumes/create |
Creates or updates a named managed volume. |
POST |
/volumes/remove |
Removes an unmounted volume. |
POST |
/volumes/snapshot |
Creates a ContainerArchive snapshot of a volume. |
POST |
/volumes/restore |
Restores a snapshot into a volume. |
GET |
/resources |
Lists provisioned per-service DB/S3 resources, but fails closed when any database is allocation-managed or quarantined because this broad route cannot carry an expected reference. |
POST |
/resources/status |
Returns exact service-resource status and is allocation-aware through expectedDatabaseAllocation. |
POST |
/resources/provision |
Provisions database and/or object storage for a service. |
POST |
/resources/deprovision |
Deletes provisioned DB/S3 resources for a service. |
POST |
/resources/database/digest |
Computes a bounded canonical content digest for exactly one service-owned or credential-binding-owned NoSQLDB database. |
POST |
/resources/database/read-access-grant/issue |
Issues or exactly replays one fixed-expiry read-only credential for an allocated database. |
POST |
/resources/database/read-access-grant/revoke |
Idempotently revokes one allocation-bound read-only credential. |
POST |
/resources/database/export |
Exports exactly one service-owned NoSQLDB database as a bounded portable payload. |
POST |
/resources/database/import |
Replaces a pre-provisioned service database under a durable fence and returns a held-publication receipt. |
POST |
/resources/database/import/commit |
Releases the exact held database only after the orchestrator durably records its receipt. |
POST |
/resources/database/backup/export |
Streams one selected service database as a bounded portable ContainerArchive closure. |
POST |
/resources/database/backup/package |
Packages one exact offline NoSQLDB database export into a durable, replayable selected closure. |
POST |
/resources/database/backup/package/release |
Releases one completed package only after the caller reports fsynced closure and receipt identities. |
POST |
/resources/database/backup/restore |
Verifies a selected closure in isolated staging and durably restores it into a pre-provisioned target database. |
POST |
/resources/snapshot |
Snapshots service DB/S3 resources. |
POST |
/resources/restore |
Restores service DB/S3 resources. |
POST |
/archive/manifest |
Returns a manifest for the local archive repository. |
POST |
/archive/object/read |
Reads an archive object as base64 with size and SHA-256. |
POST |
/archive/object/write |
Writes a validated archive object from base64. |
POST |
/archive/convert |
Converts a volume archive restored in the legacy layout of Corestore 33.x, keeping it as volume-archive.restored-legacy; answers { outcome: 'absent' | 'current' | 'converted' }. |
POST |
/archive/prune |
Prunes archive data under configured retention and free-space bounds. |
POST |
/isolated-restore/prepare |
Validates an operation-scoped grant, mappings, and archive manifest, then creates durable fenced state. |
POST |
/isolated-restore/archive/object/write |
Writes one bounded archive object chunk under a write-object grant. |
POST |
/isolated-restore/execute |
Restores only the grant-authorized scratch resources. |
POST |
/isolated-restore/status |
Returns sanitized durable progress under a status grant. |
POST |
/isolated-restore/cleanup |
Cleans staged artifacts under a cleanup grant while preserving durable fencing evidence. |
Ordinary credential rotation accepts {serviceId, capability, operationId, expectedGeneration} for a manifest-owned database or objectstorage resource. A resource with no rotation record starts at generation 0. An exact operation retry returns the same value-free receipt; the separate material route accepts {receipt} and returns the current provider-attested environment only while that receipt is active. Deprovision retires the generation before deleting the provider resource, recovers either side of manifest publication on restart, and revokes old material receipts. Reprovision retains the last generation so it cannot silently return to generation-0 credentials.
Provision resources for a service:
curl -X POST http://corestore:3000/resources/provision \
-H 'content-type: application/json' \
-H 'authorization: Bearer <CORESTORE_API_TOKEN>' \
-d '{"serviceId":"svc-123","serviceName":"api","capabilities":["database","objectstorage"]}'
The response includes service-specific environment variables such as MONGODB_URI, S3_BUCKET, AWS_ACCESS_KEY_ID, and AWS_ENDPOINT_URL.
MONGODB_URI, MONGODB_URL, and the related MONGO_* names are intentional: NoSQLDB is the database server, and workloads connect to it through the MongoDB wire protocol. They do not imply that a MongoDB server is running.
Scratch database allocations
Scratch and rehearsal databases are allocated through the database-only /resources/provision path. The provisioning body adds databaseAllocation with exactly mode: "expected-absent", purpose: "scratch-rehearsal", and a stable rehearsalId; it does not accept allocationId or an expected reference. Corestore requires NoSQLDB's authoritative allocation-fencing health contract before recording prepared state, asks NoSQLDB to allocate the tenant, durably fsyncs the manifest binding, and only then returns credentials. The response returns the collision-resistant allocationId as part of the safe allocation reference. If the allocation becomes durable but manifest publication fails, provisioning returns HTTP 503 with an explicit restart-recovery instruction; before opening the control API, startup resumes released allocations, prepared allocations whose NoSQLDB lifecycle is still allocating or active, and completed deletes. Allocation-ownership conflicts quarantine only the affected database and return HTTP 409, while provider unavailability or malformed provider state aborts startup. An exact retry after response loss or restart replays the same allocation; a new rehearsal for the same service advances the allocation generation after the prior tenant has been deprovisioned. Reuse of an allocation or rehearsal identity is rejected.
Orchestrators should import Corestore's canonical allocation normalizers and identity helpers instead of reproducing the protocol rules:
import {
coreStoreDatabaseAllocationReferencesEqual,
getCoreStoreDatabaseIdentity,
normalizeCoreStoreDatabaseAllocationReference,
normalizeCoreStoreDatabaseAllocationRequest,
} from '@serve.zone/corestore';
These helpers validate the exact allocation request/reference schemas, compare every safe reference field, and derive the same database and username identity used by Corestore provisioning. They do not expose credentials or provider-private allocation evidence.
The package root also exports validateCoreStorePersistedDatabaseAllocation, getCoreStoreDatabaseAllocationReference, and coreStoreSmartDbAllocationIdentitiesEqual for Corestore-aware provider integrations that already hold the complete persisted allocation record or NoSQLDB allocation identity. General orchestrators should use only the safe request, reference, equality, and database-identity helpers shown above; provider-state helpers accept private allocation structures and must not be used to widen API responses or logs.
{
"serviceId": "Service:scratch-123",
"serviceName": "scratch-123",
"capabilities": ["database"],
"databaseAllocation": {
"mode": "expected-absent",
"purpose": "scratch-rehearsal",
"rehearsalId": "rehearsal-123"
}
}
The safe allocation reference contains only purpose, rehearsalId, allocationId, generation, bindingSha256, and receiptSha256. It intentionally excludes the NoSQLDB principal, publication capability, database URI, password, and provider evidence. Later status, digest, portable export/import/commit, snapshot, ordinary restore, selected database backup export/restore, and deprovision requests use expectedDatabaseAllocation and require the exact returned reference whenever the database is allocated. Export operations require the source allocation reference; import, commit, ordinary restore, and backup restore require the target allocation reference. Omitted, malformed, stale, or mismatched references fail before NoSQLDB reads or writes. Existing ordinary databases without allocation metadata retain their ordinary behavior, but Corestore checks NoSQLDB's durable allocation state and quarantines a manifest that omits, corrupts, or conflicts with an active provider allocation instead of treating it as legacy.
Allocation read access grants
The grant endpoints accept at most 16 KiB of UTF-8 JSON with exactly serviceId, grantId, and the active allocation's safe expectedDatabaseAllocation reference. They are available only for allocation-managed databases and require the normal bearer control token. Corestore derives the NoSQLDB username and password with domain-separated HMAC-SHA256 over its protected persistent master secret and the exact service, allocation, and grant identity. It does not persist or log those credentials.
Issue returns serviceId, grantId, the safe databaseAllocation reference, databaseName, username, authSource, fixed roles: ["read"], issuedAt, expiresAt, and a Corestore-public-host mongodbUri. The URI is the only response field containing the password. An active exact replay returns the same grant; each grant expires after 12 minutes and is not renewable. Revoke returns only serviceId, grantId, the safe allocation reference, and revoked: true.
Issue, revoke, authentication failures, and all error responses use Cache-Control: no-store. Grant operations share the database mutation lock with allocation, import, restore, ensure, and deletion. Corestore maps NoSQLDB's exported EGRANT_* codes without parsing messages: invalid requests return 400, credential/conflict/replay-capacity failures return 409, and provider-state failures return 503. Unknown or malformed provider failures are sanitized to 503. Request disconnect, shutdown, or the five-minute provider deadline aborts the NoSQLDB management operation; NoSQLDB terminates its engine fail-stop, leaving Corestore unready until the orchestrator restarts it. NoSQLDB retains at most 1,024 retired grant IDs per allocation without eviction and rejects additional IDs when that replay set is full.
Isolated restore consumes that reference. @serve.zone/interfaces binds the safe allocation reference into the restore mapping as IIsolatedRestoreDatabaseMapping.target.databaseAllocation, and because the mapping is what the restore grant's canonical digest covers, the reference is signed authority rather than a request parameter. /isolated-restore/prepare and /isolated-restore/execute require a database mapping whose target.databaseAllocation is exactly the allocation this Corestore holds whenever the target database is allocation-managed. A missing reference returns HTTP 422 resourceMappings[<id>].target.databaseAllocation is required for an allocation-managed database; a reference naming another allocation returns HTTP 422 ... does not match the allocation this corestore holds; a reference on a database that is not allocation-managed returns HTTP 422 ... names an allocation on a database that is not allocation-managed. There is no unallocated fallback for an allocated target. The check runs again at execute time for each mapping, so an allocation rotated between prepare and execute is refused before any NoSQLDB mutation. The restore itself runs through the same per-database mutation lock and the same durable NoSQLDB ownership checks as allocation, ordinary restore, ensure, and deletion: the reference is carried into the portable import and its publication commit, and after the commit Corestore re-asserts the claim against both the manifest resource and NoSQLDB's durable allocation state, and refuses the restore if the restored database does not hold the allocation the restore was authorized against. A quarantined target service or quarantined allocation metadata still returns HTTP 409 before any of this. Isolated restore status, archive writes, cleanup, and ordinary unallocated execution are unchanged. Native /resources/restore and selected database backup restore support allocated targets as before.
Portable database handoff
The portable database endpoints are the database backup and migration boundary. Export resolves the source database only from the requested manifest serviceId; import resolves the target database and tenant user only from the pre-provisioned targetServiceId. A caller cannot select a target database name or username. For an allocated database, every source descriptor, target, held receipt, and released response carries the safe allocation reference rather than raw provider allocation evidence.
/resources/database/digest accepts either serviceId or bindingId, never both, in a body of at most 16 KiB. serviceId resolves the manifest-owned database and, for an allocated database, requires its exact expectedDatabaseAllocation; portable export follows the same rule. bindingId resolves the database an active credential binding owns from the binding's persisted authority, so an adopted legacy database keeps one digest before and after its manifest entry is removed ({ "bindingId": "..." } accepts no other field). Both resolve through the NoSQLDB durable-ownership boundary. Digest returns the resolved service/resource/database source descriptor, and NoSQLDB's smartdb.database.content-digest.v1 result. The digest covers collection names, collection options when a collection has them, exact BSON document bytes, and persisted index specifications including partial filters, while normalizing enumeration order only. BSON field order and compound-index key order remain significant. Scans are capped at 96 MiB of BSON, 10,000 collections, 1,000,000 documents, and 100,000 indexes. Compare digest.sha256 and the counters when proving equal content across source and restored databases; do not compare the complete digest objects because digest.databaseName is intentionally service-specific.
The portable payload contains canonical UTF-8 JSON bytes for smartdb.database.export.v1, with a collection's options beside its documents and indexes when it has them, encoded as base64 with an exact byte count and SHA-256. Corestore rejects non-canonical base64, invalid UTF-8, a non-canonical JSON encoding, descriptor mismatches, payloads above 16 MiB, and complete portable request bodies above 24 MiB. NoSQLDB export is additionally capped at 10,000 collections, 1,000,000 documents, and 100,000 indexes.
Corestore-native database snapshots created by /resources/snapshot and consumed by ordinary or isolated resource restore use a separate 96 MiB payload ceiling. This matches the explicit offline MongoDB-wire migration envelope without weakening the 16 MiB portable API boundary. The archive reader admits only the single trailing newline written by Corestore in addition to that payload.
Import requires a stable restoreId and positive orchestrationFence. Portable-import fences are monotonic within the portable API's own ordering domain; internal snapshot and isolated-restore attempts use a separate ordering domain and cannot make the next valid portable fence stale. Corestore durably binds the request to one NoSQLDB fence through the client's exact persistence before mutating the database. When no control record exists yet, Corestore first inspects NoSQLDB's durable released high-water mark and continues at the next token; an active publication, identity mismatch, corrupt state, or exhausted token fails before any coordination record or provider mutation is created. An exact retry after response loss or restart returns the same held receipt; after commit it returns the released receipt. Reusing a completed restoreId, submitting a stale portable orchestration fence, or conflicting with an active mutation returns 409. The imported database remains unavailable until publication is committed.
The orchestrator must follow this order:
- Call
/resources/database/importand receive the held receipt. - Persist the complete receipt and migration checkpoint durably.
- Call
/resources/database/import/commitwith that exact receipt and, for an allocated target, the separate top-levelexpectedDatabaseAllocationtarget reference.
Digest, import, export, commit, and their error responses use Cache-Control: no-store. Corestore admits only one memory-bounded portable database operation at a time, rejects concurrent mutations for the same database without queueing them, aborts response delivery on shutdown or disconnect, applies a delivery deadline, and gives lifecycle-critical NoSQLDB management and health calls a five-minute fail-stop deadline. It retains bounded monotonic restore history for each target and ordering domain. Provision, restore, isolated restore, and deprovision share the same per-database resource-mutation namespace; there is no unfenced database mutation path in normal Corestore operation.
Selected database backup closures
The database backup endpoints are the operator-scoped backup transport for database payloads larger than the 16 MiB portable handoff envelope. Export accepts a JSON object of at most 16 KiB with serviceId and backupId; it also requires the selected source database's exact expectedDatabaseAllocation when allocated. Backup restore requires the target database's exact reference inside the canonical x-corestore-database-backup-restore header. Corestore creates one database snapshot with controlled ownership tags, exports the exact snapshot closure through ContainerArchive, and returns the opaque closure with a non-cacheable receipt header on the bearer-authenticated response. The receipt's SHA-256 fields bind identity and integrity; they are not an independent signature or MAC. MongoDB wire protocol remains the workload interface before and after backup; the closure transports NoSQLDB's canonical database snapshot representation.
The underlying NoSQLDB snapshot payload is capped at the 96 MiB Corestore-native database envelope. Closure transfer is capped at 256 MiB, 4,096 immutable archive objects, 128 MiB of verified selected plaintext, a 16 KiB canonical receipt header, and five minutes without transfer progress. Restore requires one exact Content-Length, identity transfer encoding, no content encoding, and byte length equal to the receipt. It imports only into a new private disposable staging repository, validates the exact one-snapshot ownership and item contract, restores into a pre-provisioned target service database through the normal durable NoSQLDB fence, and removes staging before returning success.
The restore identity binds the backup, source and target resource identities, snapshot ID, and closure digest. Omitting restoreAttemptId preserves the original restore identity exactly. A caller can provide a canonical restoreAttemptId to select a new fenced restore attempt: retries with the same value resume or replay the same mutation, while a different value intentionally reapplies the backup to the same exact target allocation. The attempt identifier cannot change or bypass expectedDatabaseAllocation.
Selected closure is a semantic backup boundary, not a byte-level tenant-confidentiality boundary. ContainerArchive excludes unrelated snapshot manifests and unselected global-index entries, but immutable reachable packs can contain physical bytes or sidecar descriptions shared with other snapshots. Keep these artifacts inside the trusted operator backup boundary. Onebox continues to back up object storage through the streaming S3 API rather than this database-only closure endpoint.
The repository-wide /archive/manifest and /archive/object/* endpoints are legacy node-replication surfaces used by the current Coreflow backup path. They are not safe service-selected backup APIs and must not be used by new consumers; they remain only until Coreflow moves to selected closures.
Offline database export packaging
/resources/database/backup/package accepts one exact canonical smartdb.database.export.v1 file, including its single trailing LF, without reading or mutating the selected NoSQLDB database contents. It still reads current NoSQLDB ownership and allocation state to validate the source fence. The request body uses exact application/json, one exact positive Content-Length, identity transfer encoding, and no content encoding. Canonical package metadata from @serve.zone/interfaces/runtime is encoded as unpadded base64url in x-corestore-database-export-package-request. Metadata is capped at 16 KiB before encoding, input at 96 MiB plus its LF, and the resulting closure at 256 MiB and 4,096 immutable objects. Input and response delivery each fail after five minutes without progress. The control server keeps a bounded 64 KiB aggregate request-header budget so the encoded metadata and authorization header reach the route without weakening the contract limit.
Corestore validates the input length, SHA-256, UTF-8, canonical JSON round trip, database name, collection and index uniqueness, current source ownership, and exact allocation expectation. It ingests the unchanged bytes as the sole database.json / smartdb-database-export item in an attempt-private ContainerArchive repository using the configured archive encryption profile. A successful response has runtime.corestoreDatabaseBackupMediaType (application/vnd.serve-zone.corestore-database-backup), the exact closure Content-Length, and the raw opaque closure as its body. The normal closure receipt is returned in x-corestore-database-backup-receipt; x-corestore-database-export-package-response carries the canonical package completion and replay flag. Both headers use unpadded base64url canonical JSON. All package responses are non-cacheable, and failed requests close the connection so an unread rejected body cannot be reused as another HTTP request.
Each packageAttemptId has a cross-restart lease and fsynced state. Before binding a new attempt, Corestore atomically publishes a durable fixed-slot reservation for the full 256 MiB per-closure maximum. Once admitted canonical metadata is durably bound, a failed or truncated input leaves that attempt and reservation in preparing; the caller must retry the same attempt and metadata rather than minting replacement IDs. The default ceiling provides four slots, bounding admitted incomplete and completed attempts across crashes. Full capacity rejects a new attempt before creating its lease or state with HTTP 507 and PACKAGE_CAPACITY_EXHAUSTED; exact preparing/completed retries and releases remain available. Corestore fsyncs and hashes the retained closure before recording completion, then reopens, pins, and revalidates that same descriptor before every delivery. An exact retry must resend and pass validation for the same input bytes and receives the identical retained closure; conflicting reuse returns ATTEMPT_CONFLICT. A disconnected response leaves the completed attempt replayable.
The caller must fsync the received closure and the exact canonical receipt bytes produced by encodeCorestoreDatabaseBackupReceipt() before posting the canonical release request to /resources/database/backup/package/release. Release requires exact application/json, one exact positive Content-Length, identity transfer encoding, no content encoding, and canonical JSON within the 16 KiB control limit. It returns a JSON release response. Corestore durably records release intent before deleting the attempt-private repository and closure, fsyncs the artifact root, and retains a small terminal state for exact release replay. Package retries after successful release return HTTP 410. The bearer control token is the transport authority; Corestore binds but does not independently authenticate upstream settlement receipt digests carried by the package request.
Package clients can import coreStoreDatabaseExportPackageRequestHeader, coreStoreDatabaseExportPackageResponseHeader, coreStoreDatabaseExportPackageReleasePath, and coreStoreDatabaseExportPackageMaximumHeaderBytes from @serve.zone/corestore instead of duplicating the public transport names and header budget.
The following Node.js outline shows the complete request shapes and the required canonical encoding, verification, durable write, and release order. Values such as allocation references and upstream settlement digests must come from the orchestrator's approved operation; they are not invented by the package client.
import { createHash } from 'node:crypto';
import { open } from 'node:fs/promises';
import {
coreStoreDatabaseExportPackageRequestHeader,
coreStoreDatabaseExportPackageResponseHeader,
coreStoreDatabaseExportPackageReleasePath,
type ICoreStoreDatabaseExportPackageRequest,
type ICoreStoreDatabaseExportPackageReleaseRequest,
} from '@serve.zone/corestore';
import * as runtime from '@serve.zone/interfaces/runtime';
const sha256 = (bytes: Uint8Array) => createHash('sha256').update(bytes).digest('hex');
const databaseExportBytes = Buffer.from(`${canonicalSmartDbExportJson}\n`, 'utf8');
const packageRequest: ICoreStoreDatabaseExportPackageRequest = {
kind: 'corestore-database-export-package-request',
packageAttemptId,
packageBackupId,
input: {
format: 'smartdb.database.export.v1',
databaseName,
byteLength: databaseExportBytes.length,
sha256: sha256(databaseExportBytes),
lineFeedTerminator: 'single-lf',
},
approvedSourceBackup: {
backupId: approvedBackupId,
sourceServiceId,
snapshotId: approvedSnapshotId,
backupReceiptSha256: approvedBackupReceiptSha256,
},
completeOutputReceiptSha256,
allocationExpectation: {
expectation: 'exact',
allocation: {
purpose: 'scratch-rehearsal',
rehearsalId,
allocationId,
generation,
bindingSha256,
receiptSha256: allocationReceiptSha256,
},
},
};
const packageRequestBytes = Buffer.from(
runtime.encodeCorestoreDatabaseExportPackageRequest(packageRequest),
);
const packageHttpResponse = await fetch(`${corestoreOrigin}/resources/database/backup/package`, {
method: 'POST',
headers: {
authorization: `Bearer ${corestoreToken}`,
'content-type': 'application/json',
'content-length': String(databaseExportBytes.length),
[coreStoreDatabaseExportPackageRequestHeader]: packageRequestBytes.toString('base64url'),
},
body: new Uint8Array(databaseExportBytes),
});
if (!packageHttpResponse.ok) throw new Error(`package failed: ${packageHttpResponse.status}`);
const encodedPackageResponse = packageHttpResponse.headers.get(
coreStoreDatabaseExportPackageResponseHeader,
);
const encodedReceipt = packageHttpResponse.headers.get('x-corestore-database-backup-receipt');
if (!encodedPackageResponse || !encodedReceipt) throw new Error('package headers are missing');
const packageResponseBytes = Buffer.from(encodedPackageResponse, 'base64url');
if (packageResponseBytes.toString('base64url') !== encodedPackageResponse) {
throw new Error('package response header is not canonical base64url');
}
const packageResponse = runtime.normalizeCorestoreDatabaseExportPackageResponseForRequest(
JSON.parse(packageResponseBytes.toString('utf8')),
packageRequest,
);
const closureBytes = Buffer.from(await packageHttpResponse.arrayBuffer());
if (
closureBytes.length !== packageResponse.completion.closureByteLength
|| sha256(closureBytes) !== packageResponse.completion.closureSha256
) {
throw new Error('package closure identity does not match its completion');
}
const receiptBytes = Buffer.from(
runtime.encodeCorestoreDatabaseBackupReceipt(packageResponse.completion.receipt),
);
if (receiptBytes.toString('base64url') !== encodedReceipt) {
throw new Error('package receipt header is not canonical or does not match the completion');
}
for (const [fileName, bytes] of [
['database.closure', closureBytes],
['receipt.json', receiptBytes],
] as const) {
const handle = await open(fileName, 'wx', 0o600);
try {
await handle.writeFile(bytes);
await handle.sync();
} finally {
await handle.close();
}
}
const outputDirectory = await open('.', 'r');
try {
await outputDirectory.sync();
} finally {
await outputDirectory.close();
}
const releaseRequest: ICoreStoreDatabaseExportPackageReleaseRequest = {
kind: 'corestore-database-export-package-release-request',
packageAttemptId,
requestSha256: packageResponse.completion.requestSha256,
completionSha256: sha256(
runtime.encodeCorestoreDatabaseExportPackageCompletion(packageResponse.completion),
),
callerFsyncedClosureByteLength: closureBytes.length,
callerFsyncedClosureSha256: sha256(closureBytes),
callerFsyncedReceiptByteLength: receiptBytes.length,
callerFsyncedReceiptSha256: sha256(receiptBytes),
callerFsyncedAt: Math.max(Date.now(), packageResponse.completion.completedAt),
};
const releaseBytes = Buffer.from(
runtime.encodeCorestoreDatabaseExportPackageReleaseRequest(releaseRequest),
);
const releaseRequestHandle = await open('release-request.json', 'wx', 0o600);
try {
await releaseRequestHandle.writeFile(releaseBytes);
await releaseRequestHandle.sync();
} finally {
await releaseRequestHandle.close();
}
const releaseDirectory = await open('.', 'r');
try {
await releaseDirectory.sync();
} finally {
await releaseDirectory.close();
}
const releaseHttpResponse = await fetch(`${corestoreOrigin}${coreStoreDatabaseExportPackageReleasePath}`, {
method: 'POST',
headers: {
authorization: `Bearer ${corestoreToken}`,
'content-type': 'application/json',
'content-length': String(releaseBytes.length),
},
body: new Uint8Array(releaseBytes),
});
if (!releaseHttpResponse.ok) throw new Error(`release failed: ${releaseHttpResponse.status}`);
After the first release POST, retries must read and resend the exact fsynced release-request.json bytes. Do not regenerate callerFsyncedAt after an ambiguous response because any byte change is conflicting reuse and returns 409 ATTEMPT_CONFLICT.
Named storage bindings
The storage-binding API consumes the shared @serve.zone/interfaces App Store request/class shapes and returns platform.TResolvedStorageBinding. The request contains portable policy only. Provider names, host paths, network shares, authentication mechanisms, mount options, endpoint overrides, and bucket names are rejected as unknown physical fields.
Example object-storage reconciliation:
{
"bindingId": "binding:backup-archive",
"serviceId": "Service:example",
"generation": 1,
"policy": {
"classId": "corestore.object-storage.standard",
"revision": "1"
},
"storageClass": {
"kind": "objectStorage",
"purpose": "backup",
"required": {
"durability": "persistent"
}
},
"request": {
"id": "backup-archive",
"kind": "objectStorage",
"storageClass": "archiveStorage",
"reclaimPolicy": "retain",
"accessMode": "readWrite",
"delivery": {
"type": "launcher-environment",
"keys": {
"endpoint": "ARCHIVE_S3_ENDPOINT",
"bucket": "ARCHIVE_S3_BUCKET",
"region": "ARCHIVE_S3_REGION",
"accessKeyId": "ARCHIVE_S3_ACCESS_KEY_ID",
"secretAccessKey": "ARCHIVE_S3_SECRET_ACCESS_KEY"
}
}
},
"credentialManagementScope": "platform:object-storage-binding"
}
The request digest is the contract's value, not a Corestore-local one: it is derived with the interfaces createStorageRequestSha256, a bare lowercase hex SHA-256 over the canonical request alone, so a consumer that re-derives it from the request it declared reaches the same digest. Callers cannot supply one. A request is only admitted if that function can digest it, because the request subtree is normalized by the interfaces normalizeStorageRequest, the very judgement createStorageRequestSha256 derives the digest from: delivery environment keys match ^[A-Z_][A-Z0-9_]{0,255}$, a declared protection block carries at least one member, and no member may be present with an undefined value. A contract refusal is answered as a malformed request (400) carrying the contract's own message, which names the failing field. On top of the contract Corestore applies only the rules it owns: the request id ^[a-z][a-z0-9-]{0,62}$ and class key ^[A-Za-z][A-Za-z0-9-]{0,62}$ that name derived volumes, buckets and credential resources, a single credential file directly below /run/secrets that is never the platform's own /run/secrets/secret.json, and sessionToken delivery refused as an unsupported capability (422) because Corestore issues long-lived pool credentials and has no session-token source. Stable (serviceId, request.id) and binding identities are unique. The same generation and intent is idempotent, stale generations fail, and changed intent requires an explicit migration. Binding records are strictly revalidated on every control-record read and write, updated through revision compare-and-swap, and fenced by operation leases. The value-free credentialManagementScope describes platform ownership without storing or coupling the binding to a secret record.
Release is a durable replay-safe lifecycle: the release intent is recorded before provider deletion, provider deletion is idempotently retried after a crash, and terminal retained or released tombstones prevent allocation-id reuse. Reconcile cannot resume after release starts. Provider and persistence details are reduced to fixed failure messages; binding responses and persisted failures never expose provider credential material or raw provider errors.
Object-storage placement is the selected policy class revision: portable workload requirements never contain host paths, NFS exports, SMB shares, or provider endpoint overrides. Pool catalog records, binding placement, migration ownership, and migration state are authoritative control records and survive process restarts.
Changing a live object's class is an explicit migration, never an in-place reconcile. Corestore creates a hidden destination, copies every object with exact create-only publication and preserved supported metadata, asks the orchestrator to quiesce the consumer, establishes a source seal, verifies the destination manifest, publishes the durable binding cutover, waits for consumer activation evidence, and only then performs fenced source cleanup. Abort is allowed only before the commit point; after cutover, recovery rolls forward.
The configured classes deliberately advertise only guarantees the named lifecycle enforces:
- Filesystem: persistent, single-node,
ReadWriteOnce, standard performance. - Default object storage: persistent, single-node, read-write, standard performance, with optional compliance retention.
- Additional object-storage pools may advertise exactly one of standard, high-IOPS, or capacity performance when the operator maps that class to a matching physical backend.
- No hard quotas, snapshot/backup guarantee, versioning, read-only object policy, multi-node topology, or transport-encryption guarantee yet.
- Every hard capacity quantity is rejected until Corestore can reserve and report an enforced byte limit. Session-token delivery is rejected until Corestore can issue actual session credentials.
Reconcile, get, and list responses never contain provider credential values. The separate credentials endpoint requires the current service id and generation, returns semantic credential material, and is non-cacheable. The fulfillment adapter maps that material to the App Store request's launcher-environment or file delivery contract.
Runtime credential bindings
The credential-binding API is the Coreflow handoff for @serve.zone/interfaces 32. POST /control/credential-bindings/reconcile accepts only the exact value-free binding request — every object-storage request bound to its durable bucketName — and its current publication grant. GET /control/credential-bindings, optionally filtered by serviceId, and GET /control/credential-bindings/<id> return status and provider authority without credential values. POST /control/credential-bindings/<id>/material requires the exact current grant and is the only route that returns plaintext; successful, authentication-failure, and error responses are non-cacheable. A grant's reconciliation generation is ordered against the grant the binding already holds: an older one answers 409 credential reconciliation generation is stale, and a different grant at the same generation 409 credential reconciliation generation conflicts. A binding Corestore does not hold yet accepts any positive generation, because the issuer replaces an unused grant when it expires or its request changes and cannot know whether an earlier one arrived.
A database binding request must state its endpoint (IPlatformDatabaseEndpoint { host, port }, the corestoreDatabase of the node the binding is placed on): a request without one answers 409 database credential binding request states no endpoint before anything is stored, and so does a material read of a binding recorded before requests stated one. The material's eleven MONGODB_*/MONGO_* values are createCorestoreDatabaseCredentialValues(endpoint, …) of @serve.zone/interfaces — the shared URI form, addressed to that endpoint and never to CORESTORE_PUBLIC_HOST, which on a converted root is an identity a workload on another runtime cannot resolve. The endpoint is the one member a binding's request may change: a newer grant (higher reconciliation generation) for the same request with another endpoint re-addresses the binding, keeping its database, user and password, and the superseded grant stops returning material. Any other change still answers 409 credential binding request cannot change.
Legacy ordinary resources with credentials already delivered through /resources/provision use a separate, explicit rotation API. POST /control/ordinary-credentials/rotate accepts { "serviceId": "...", "capability": "database" | "objectstorage", "operationId": "...", "expectedGeneration": 0 }; absence of an ordinary rotation record means generation zero. The response is { "receipt": { "serviceId": "...", "capability": "...", "resourceName": "...", "operationId": "...", "previousGeneration": 0, "generation": 1, "phase": "active" } }, without credentials. Exact operation replay returns the same current receipt; stale generations, conflicting operation identities, allocation-managed databases and binding-managed resources fail closed. Corestore claims the operation through SmartData CAS, verifies a fenced provider ensure without deleting documents or objects, persists the new generation, updates its manifest, and recovers interrupted phases on startup. For object storage the bucket access-key ID stays stable while the secret changes; the old key/secret pair stops authenticating.
Only after durably storing that receipt should a consumer call authenticated POST /control/ordinary-credentials/material with { "receipt": <exact receipt> }. It returns { "material": { "capability": "...", "generation": 1, "env": { ... } } } with the canonical database or S3 environment values. Corestore attests the current provider credential and exact manifest before delivery. Both routes send Cache-Control: no-store and Pragma: no-cache. A consumer must durably refresh its own resource record and workload secrets before restarting writers; Corestore does not mutate consumer state.
Corestore derives database and object-storage credentials from its protected master secret and never persists them in binding records. Every active material read replays the exact NoSQLDB ensure fence to reapply and prove the expected database password, username, roles, and receipt, or validates the exact ObjectStorage bucket credential and retention evidence. Generic provision, import, restore, migration cleanup, and deprovision paths are rejected when their target is credential-managed.
Adopting legacy /resources/provision resources
A service whose database or bucket was provisioned through /resources/provision keeps its data when it moves to a credential binding, but only after the operator authorizes that exact move. Without an authorization, a new binding for a service that still owns a legacy resource of the same capability answers 409 credential binding service owns a legacy resource that is not authorized for adoption: a fresh binding would otherwise start from an empty database or bucket while the service's data stays behind the manifest entry. The flow follows the shared contracts of @serve.zone/interfaces ("Corestore Legacy Resource Adoption"): Cloudly reads the inventory below, creates the adopting bindings and answers the plan this migration reads.
Authenticated GET /control/legacy-resources answers ICorestoreLegacyResourceInventory: { "services": [...] }, every service that still holds a legacy resource, sorted by service id, each with its database resource (databaseName) before its objectstorage resource (bucketName and the retention intent, or null). No credential value appears. Each resource carries its adoption: { "state": "authorized", "bindingId": "..." } once an adoption is authorized, otherwise { "state": "unadopted", "blocker": ... }, where the blocker names the first rule that would make the offline adoption refuse it, or null when none would: not-adoptable (an allocation-managed database, another provider, a resource name that differs from its database or bucket name, or a bucket name that is not a canonical S3 name), owned-by-binding (a credential binding owns the service capability or the resource, a named storage binding owns the bucket, or another adoption names the resource) or ordinary-credentials-rotated. A consumed adoption removes the manifest entry, so it no longer appears. An inventory the shared contract cannot state, such as a legacy service id that is not a canonical identifier, answers 500 naming the reason instead of a shortened list.
Authorize adoptions offline while Corestore is stopped, after engine-activate or after this release has started once on the root: with the npm package or the image, pnpm run migrate:legacy-resource-adoption (node migration.js legacy-resource-adoption); with the self-contained host executable, corestore legacy-resource-adoption (see Offline operator commands). Both run the same command. It requires absolute CORESTORE_DATA_DIR, CORESTORE_ADOPTION_PLAN_PATH pointing at a private mode-0600 JSON plan, and CORESTORE_ADOPTION_CORESTORE_STOPPED=true; CORESTORE_DB_ROOT_USER names the control-store root user when the root does not use the default corestore_root, and the password comes only from the root's corestore-secret.json. The plan is an array of exact { "serviceId": "...", "capability": "database" | "objectstorage", "bindingId": "..." } entries (ICorestoreLegacyResourceAdoptionPlanEntry), each with "successorServiceId": "..." when a successor adopts, validated with the shared validateCorestoreLegacyResourceAdoptionPlan: non-empty, at most 4096 entries, canonical identifiers, no legacy service capability, adopting service capability or binding id twice. serviceId always owns the legacy resource. Without successorServiceId the binding belongs to that service; with it, the binding belongs to the successor, a fresh service that replaces the legacy one and takes over its data. Every entry is checked before anything is written; a refusal names its reason (corestore-legacy-resource-adoption-refused: ...). The operation refuses a missing legacy resource, an allocation-managed database, a bucket whose name is not a canonical S3 name, a resource with ordinary credential rotation history, a binding id, service capability or resource that a credential binding already owns, a bucket that a named storage binding owns, a successor that owns a legacy resource of the capability itself, and a second authorization that differs from an existing one, including one of the same resource for another service. An identical replay answers unchanged. It writes one CoreStoreLegacyResourceAdoption record per entry, keyed by the adopting binding's service and capability, holding the legacy identity as the manifest states it: database name and user, or bucket name, access-key id and the retention intent and receipt. A successor's record also names the legacy owner as legacyServiceId, and the inventory lists the legacy owner's resource as authorized for the successor's binding. If a 32 release already created a fence lineage for the bucket under the manifest authority, the operation moves that lineage to the adopting binding's authority. On success stdout carries only the value-free result (ICoreStoreLegacyResourceAdoptionResult: status, planSha256 and per entry the binding, resource name, legacyResourceSha256, authorization written or unchanged and fenceLineage absent, transferred or unchanged) as one line of JSON, and the exit code is 0; diagnostics go to stderr. On failure stderr carries Corestore legacy resource adoption failed: <reason> and the exit code is 1. CORESTORE_ADOPTION_CORESTORE_STOPPED=true is the operator's assertion; NoSQLDB enforces it: a running Corestore holds the root's exclusive lease, so the command fails with NoSQLDB file storage root is already owned by another process before it reads or writes any control data.
From that moment only the adopting binding may mutate the resource: every path guarded for credential-managed resources (provision, deprovision, ordinary rotation, import, restore and migration cleanup) answers 409 for it. When the binding is reconciled with exactly the authorized bindingId:
- The resource authority is the legacy identity. The database keeps its name and user; its password derives from the binding's service id. For the legacy owner's own binding that is exactly the legacy password, so already delivered database credentials stay valid; a successor's binding derives it from the successor, so the legacy owner's database password stops authenticating. The bucket keeps its name, objects, retention sentinel and every object's retain-until.
- Before any record or provider changes, Corestore requires the current manifest identity to equal the authorized one, the database tenant to exist, the requested bucket name to be the legacy bucket, the requested
objectstorageRetentionto equal the legacy intent exactly, and the provider's retention receipt to equal the manifest receipt. Any difference answers 409 and leaves the resource untouched. - Every adoption refusal is a 409 whose body is
{ "ok": false, "error": "...", "code": "..." },codefrom the shared binding status reasons, whichreadCorestoreLegacyAdoptionRefusalreads:legacy-adoption-pendingfor a service whose legacy resource no adoption authorizes;legacy-adoption-conflictfor an adoption authorized for another binding or already consumed;legacy-adoption-mismatchwhen the legacy identity changed since authorization, the database tenant is absent, or the bucket name, retention intent or retention receipt differs. - The bucket keeps its legacy access-key id and its secret is replaced by the binding's derived secret; the legacy secret stops authenticating. ObjectStorage binds a bucket's exact-owned credential to its access-key id and rotates only the secret under a fence. The binding's retention evidence carries the legacy receipt, which must equal the provider receipt after the fenced ensure. A bucket whose provider fence still carries the pre-32 scope continues in that lineage.
- The binding's activation and the adoption's consumption are one transaction. Corestore then removes the legacy manifest entry, the legacy owner's for a successor's binding; start-up finishes a removal a crash interrupted before the control API opens, and a replayed reconcile of the active binding finishes it too.
Interfaces 32 defines publication grants but no deletion grant. Corestore therefore has retain-only credential lifecycle authority: /retain writes a permanent tombstone and leaves the database or bucket intact. Omission from desired state, disablement, grant expiry, process restart, and retained status never authorize provider deletion or credential revocation. A retained resource remains protected from generic mutation and cannot be reconciled or have material delivered again.
Each isolated-restore request body carries a restoreGrant compact JWT in addition to its exact request fields. Grants use RS256, identify a configured key by kid, authorize exactly one operation, expire within five minutes by default, bind the target cluster and node, and bind the canonical resource-mapping and archive-manifest hashes. Corestore rejects a grant when any bound request value differs.
Snapshot a volume:
curl -X POST http://corestore:3000/volumes/snapshot \
-H 'content-type: application/json' \
-H 'authorization: Bearer <CORESTORE_API_TOKEN>' \
-d '{"name":"sz-api-data-abc123","snapshotName":"before-deploy"}'
Restore a volume snapshot:
curl -X POST http://corestore:3000/volumes/restore \
-H 'content-type: application/json' \
-H 'authorization: Bearer <CORESTORE_API_TOKEN>' \
-d '{"name":"sz-api-data-abc123","snapshotId":"<snapshot-id>","clear":true}'
Docker Volume Driver
Corestore implements these Docker VolumeDriver endpoints over its Unix socket:
/Plugin.Activate/VolumeDriver.Capabilities/VolumeDriver.Create/VolumeDriver.Remove/VolumeDriver.Mount/VolumeDriver.Unmount/VolumeDriver.Path/VolumeDriver.Get/VolumeDriver.List
The Corestore service must bind mount /run/docker/plugins from the host so Docker can discover /run/docker/plugins/corestore.sock. On a host without Docker, set CORESTORE_VOLUME_PLUGIN=disabled: Corestore then creates no socket, and GET /health/details reports volumes.pluginEnabled: false and volumes.pluginSocketPath: null.
Volume mountpoints are real host directories under:
<CORESTORE_DATA_DIR>/volumes/<safe-volume-name>-<hash>/data
Docker bind-mounts those paths into workload containers. Corestore tracks mount ids, service metadata, backup flags, and snapshot history in corestore-manifest.json.
Coreflow Integration
The intended platform behavior is:
- Coreflow deploys Corestore as a global service so each workload node has a local storage provider.
- New portable filesystem and object-storage requests are matched against
/control/storage/capabilitiesand reconciled through stable v2 binding ids. - Coreflow obtains object-storage credential values only from the authenticated non-cacheable credentials endpoint, then applies the declared
launcher-environmentorfiledelivery contract. - Coreflow reconciles Interfaces 32 runtime credential bindings through
/control/credential-bindings/reconcile, observes them through the list/status routes, obtains values only from/<id>/material, and calls/<id>/retainfor omission or disablement; it must not infer deletion authority from an expired or absent grant. - The resolved filesystem
resourceRefis handed to the runtime volume attachment; physical attachment details do not enter App Store manifests. - Workload platform bindings for
databaseandobjectstoragecall/resources/provision. - First-class workload volumes use Docker
DriverConfig.Name = 'corestore'by default. - Backup orchestration snapshots volumes through
/volumes/snapshotand service resources through/resources/snapshot; portable database transfer uses/resources/database/export. - Ordinary local restore orchestration uses
/volumes/restore,/resources/restore, and archive object read/write endpoints. Database restore is internally routed through the same durable import/commit fence as portable handoff. - Authorized scratch-environment recovery uses the signed
/isolated-restore/*prepare, object-write, execute, status, and cleanup sequence. Each phase receives a new operation-scoped grant from the control plane.
Host Service
Corestore runs as a plain host process under a service manager such as systemd, without Docker. It ships two ways:
-
Self-contained executable (the host artifact). Each release publishes a sealed TsPack set on its Gitea release:
corestore-host-<version>-linux-amd64.tar.gz, the same forlinux-arm64,tspack-manifest.jsonandSHA256SUMS.txt. A bundle holds no Node.js and needs none:File Content corestoreCorestore compiled with Deno 2.9.7 ( tsdeno compile), at most 200 MiBcorestore-nosqldb,corestore-objectstorage,corestore-containerarchiveThe static musl engines of the pinned @lossless.org/nosqldb,@lossless.org/objectstorageand@serve.zone/containerarchive, each with its.tsrust-build.jsonprovenancethird-party-notices.txt,third-party-notices.jsonThe Deno runtime (with its corresponding-source and LGPL relinking statement) and every embedded npm package notices/<engine>/…The notices each engine package ships for its native code license.mdCorestore's license The executable selects each engine by its exact path beside itself; a missing or unusable engine fails startup (
ERR_RUST_BINARY_EXPLICIT_PATH_INVALID) instead of being searched for. It writes nothing next to itself: the install directory can be read-only.corestore --versionprints@serve.zone/corestore v<version>. Verify a download against the manifest digest from a trusted channel with@git.zone/tspack(verifyBundle/extractBundle). -
npm package:
node <install>/node_modules/@serve.zone/corestore/cli.js(thecorestorebin).
Either way the process owns its lifecycle:
-
Readiness.
GET http://<CORESTORE_BIND_ADDRESS>:<CORESTORE_CONTROL_PORT>/healthanswers200 {"ok":true}only while Corestore is running: NoSQLDB, every object-storage pool and the storage-binding registry are up and startup has finished. It answers503while starting and stopping. It needs no token. The log linecorestore successfully startedfollows readiness but is not the contract. Under aType=notifyunit the self-contained executable sends systemdREADY=1at that moment (overNOTIFY_SOCKET, which must be a filesystem path); the npm package does not notify, so run it withType=execand poll/health. -
Start-up progress. A start-up step whose duration grows with the data, today the first start's volume archive conversion (see Upgrading from 33.x), reports its progress. Every 10 seconds the host executable checks whether that step advanced since the last check; only if it did, it sends one notification with
EXTEND_TIMEOUT_USEC=30000000(30 seconds, three checks) and the latest progress asSTATUS=, never with a path:- while the files are copied (or, on a resumed conversion, compared),
STATUS=converting volume archive: <files done> of <files total> files (<done> of <total> GiB), GiB with two decimals; - while the converted archive's index is built, up to its rename,
STATUS=building volume archive index: <read>/<total> segments read, <written> written, <verified> verified, counting the legacy index segments read, the new index's segments written and those read back. A resumed conversion whose new index was already written starts here, with0/0segments read.
Any advance counts: a file or byte copied or compared, the move to the index build, or an index segment read, written or read back. containerarchive runs at most one index segment (128 MiB at most) or one fixed set of metadata steps between two of its reports. systemd moves the start deadline to the later of the current deadline and a notification's arrival plus its extension, so the deadlines are:
- the unit's base
TimeoutStartSeccovers a start without conversion, and on a start with one everything before its first progress notification: start-up up to the conversion, the free-space estimate and the copy's planning (both list every file of the archive, so this part grows with the number of files, not their size), and up to one 10-second check; - while the step advances, through its copy and its index build, the deadline moves with its progress;
- once the step has finished, one more notification sends
EXTEND_TIMEOUT_USEC=180000000(the recommended base of 180 seconds) withSTATUS=starting after volume archive conversion, so the rest of start-up gets the budget of a start without conversion; a step that finished before its first notification never moved the deadline and sends none, the base still governs; - a step that stalls sends nothing, so the start ends about 30 seconds after its last notification (up to 40 seconds after its last progress), or at the base deadline if that is later. A step that fails sends no further extension.
No other start-up step extends the deadline. The checks stop before
READY=1and when start-up fails. A failed start-up sendsSTATUS=startup failed: <name>, the name of a migration refusal (such ascorestore-migration-archive-space-insufficient), else the error's code or class name, never its message.systemctl statusshows the latestSTATUS=. - while the files are copied (or, on a resumed conversion, compared),
-
Stop.
SIGTERMorSIGINTstops Corestore gracefully, also during startup: listeners close, ObjectStorage drains (bounded to 30 seconds per pool) and NoSQLDB stops. A volume archive conversion under way stops before its next file, or during its index build before the new index is written, and resumes on the next start. A second signal while stopping is ignored; the service manager's stop timeout is the escalation. -
Exit codes.
0: stopped cleanly after a signal (corestore stopped).1: invalid configuration, failed startup (including a refused storage root) or incomplete shutdown. -
Child processes. NoSQLDB, ObjectStorage and ContainerArchive run as native child processes that Corestore stops itself. Send the stop signal to the main process only (
KillMode=mixed); signalling the whole control group at once cuts their shutdown short.
Unit contract
| Setting | Value |
|---|---|
ExecStart |
<release dir>/corestore of the pinned host bundle; for the npm package node <install>/node_modules/@serve.zone/corestore/cli.js. No arguments. |
User |
The owner of the data root. Converted fleet roots belong to root. |
Type |
notify with NotifyAccess=main for the host executable; exec for the npm package. Readiness is also GET /health = 200. |
KillSignal / KillMode |
SIGTERM / mixed. |
TimeoutStartSec |
A base for a start without conversion, for example 180. On the start that converts the volume archive it must also cover everything before the conversion's first progress notification: the earlier start-up work, the free-space estimate and the copy's planning, which each list every file of the archive (so size it for the number of files, not their bytes), and up to one 10-second check. From then on the conversion extends the deadline while its copy or its index build advances and gives the rest of start-up a fresh 180-second budget when it finishes (see Start-up progress). A stalled conversion fails the start about 30 seconds after its last extension. |
TimeoutStopSec |
30 seconds per object-storage pool (pools stop one after another) plus a margin for NoSQLDB, for example 120. |
Restart |
on-failure. |
| Credential | LoadCredentialEncrypted=corestore-control-token:<sealed file> (or LoadCredential=). |
CORESTORE_API_TOKEN_FILE |
/run/credentials/<unit name>/corestore-control-token, the credential's literal path. |
CORESTORE_VOLUME_PLUGIN |
disabled. |
CORESTORE_DATA_DIR |
The data root, at the path it was created under (see below). |
CORESTORE_BIND_ADDRESS |
The node's service address. Control, S3 and NoSQLDB listen on it. |
CORESTORE_CONTROL_PORT / CORESTORE_S3_PORT / CORESTORE_DB_PORT |
The ports the root was created with (defaults 3000 / 9000 / 27017). |
CORESTORE_PUBLIC_HOST, CORESTORE_REGION, CORESTORE_DB_ROOT_USER, CORESTORE_CLUSTER_ID, CORESTORE_NODE_NAME, CORESTORE_OBJECT_STORAGE_POOLS_JSON |
Exactly the values the root was created with; they are part of its identity. |
The token file follows the CORESTORE_API_TOKEN_FILE rules: a regular, non-symlink file with one
token and at most one trailing newline. A systemd credential file meets them. Never put the token in
Environment=.
Keeping the root identity
NoSQLDB binds its storage root to the root's canonical path as the Corestore process sees it,
plus the device and inode of <CORESTORE_DATA_DIR>/smartdb. A root that a container created at
/data/corestore over a bind-mounted host directory is therefore bound to
/data/corestore/smartdb, not to the host directory's own path. Started at the host directory's
path, Corestore refuses the root when NoSQLDB starts (fencing profile is malformed or belongs to a different root, exit code 1); the root stays usable at its recorded path.
Run the host service at the recorded path instead. With the runtime stopped, rename the host directory to that path on the same filesystem, so every inode is kept:
stat -c %dof the host directory's parent and of the new parent (/data) must print the same device, the new parent must not be a symlink, and the target (/data/corestore) must not exist.- Rename with
rename(2)semantics only, for examplepython3 -c 'import os,sys; os.rename(sys.argv[1], sys.argv[2])' <host dir> /data/corestore. Do not usemv: across filesystems it copies, which changes every inode. - Start the unit with
CORESTORE_DATA_DIR=/data/corestore.
A rename keeps every file's path relative to the root and every inode, so NoSQLDB's binding, the
object-storage pool catalog and every other recorded path stay valid. test/test.hostservice.node.ts
proves this against a root the 33.0.0 release created inside a mount namespace: the host path is
refused (bindingState: foreign, bindingChange: path), the renamed root matches
(bindingState: matching, bindingChange: none) with the same provider root id, serves its data
and stops with exit code 0.
Offline operator commands
The host executable runs one offline operator command instead of the service when it gets the
command's name as its only argument; the unit's ExecStart passes none.
| Command | Runs |
|---|---|
corestore legacy-resource-adoption |
The authorized legacy resource adoption, exactly as migration.js legacy-resource-adoption runs it: the same environment, refusals, output and exit codes (see Adopting legacy /resources/provision resources). It starts the corestore-nosqldb beside the executable on the data root through a private temporary Unix socket and starts no listener, ObjectStorage or ContainerArchive. |
Any other arguments exit with code 1 before anything is read
(corestore: unknown arguments; ...). The host executable offers no other operation of
migration.js or adoption.js. The engine conversion (engine-*) and the prepared filesystem
volume adoption bring a Docker-era root to the current engine and layout, so they run with the
package or image of this release before the root moves to the host service; the MongoDB-wire
migration copies a workload's database from an external source and runs with the npm package of
this release.
Run a command with the unit stopped, as the unit's User (the owner of the root), with the
unit's own values for CORESTORE_DATA_DIR and, when the unit sets it, CORESTORE_DB_ROOT_USER;
systemctl show -P Environment <unit> lists them. The command reads no other variable of the
unit and needs no control token. For the adoption:
-
Start the unit once on this release if it has not run on the root yet, so its data migrations have run; the adoption refuses a root with pending steps.
-
systemctl stop <unit>and wait until it is inactive. -
Write the plan into a mode-0600 file owned by that user, for example created with
install -m 0600 /dev/null <plan path>. -
Run the command with the unit's values:
CORESTORE_DATA_DIR=<the unit's CORESTORE_DATA_DIR> \ CORESTORE_ADOPTION_PLAN_PATH=<plan path> \ CORESTORE_ADOPTION_CORESTORE_STOPPED=true \ <release dir>/corestore legacy-resource-adoption -
Keep the stdout line (it carries no credential), remove the plan and start the unit again.
If the unit starts while the command runs, whichever process holds the root's lease first keeps it: the unit's start fails while the command runs, or the command refuses without changing anything.
Rollback
The rollback artifact is the previous release's bundle or npm package, kept on the host beside the
current one; switching back changes ExecStart only. The rollback floor is 34.0.0 once 34.0.0
has started on a data directory: its first start converts the volume archive (see
Upgrading from 33.x), and a 33.x release cannot read the converted archive.
Rolling back below 34.0.0 therefore also means restoring the archive from the kept rollback copy,
as described there. A release older than 33.3.0 ignores CORESTORE_VOLUME_PLUGIN and creates
/run/docker/plugins/corestore.sock again.
Docker Image
pnpm run build:docker
The image exposes 3000, 9000, and 27017 and stores runtime data under /data/corestore unless CORESTORE_DATA_DIR overrides it.
The image is built from Dockerfile_##version##, so tsdocker publishes it under the release version tag only — code.foss.global/serve.zone/corestore:<version> with the version from package.json. From 32.0.0 on Corestore publishes no latest tag; the existing corestore:latest stays frozen at the 7.0.0 build for consumers that still pull it by that name, so pin Corestore by version. Both version labels stay out of the Dockerfile on purpose: tsdocker injects version and org.opencontainers.image.version from package.json on every build path, and a literal here would drift from what actually shipped.
Development
Common commands:
pnpm install
pnpm build
pnpm test
pnpm run watch
pnpm run build:host # compile both executables and pack an unpublishable host bundle set
The host release is built in Gitea CI from the version tag: scripts/release-host.mjs builds,
compiles with tsdeno (Deno 2.9.7, embedding exactly the npm versions pnpm-lock.yaml pins)
and packs with tspack; scripts/package-host.mjs --release --reuse verifies the retained set.
.gitea/workflows/release.yml and scripts/gitzone-*.mjs are generated by
gitzone format --only assets. pnpm-lock.yaml is the only lockfile: there is no deno.lock to
reseed, and tsdeno refuses --lock and --frozen in a compile target's permissions.
Important files:
| Path | Purpose |
|---|---|
ts/index.ts |
CLI startup wrapper exporting CoreStore, runCli, and stop. |
ts/corestore.classes.corestore.ts |
Main runtime, control API, VolumeDriver API, provisioning, snapshots, and archive replication. |
ts/corestore.interfaces.ts |
Request, response, manifest, resource, and snapshot types. |
ts/corestore.storage.ts |
Strict portable v2 contract normalization, capability negotiation, digests, and collision checks. |
ts/corestore.classes.storagebindingstore.ts |
Authoritative named-binding records and unique identities, persisted through the NoSQL client. |
ts/corestore.objectstoragepools.ts |
Strict physical pool configuration, portable class mapping, and immutable pool identities. |
ts/corestore.objectstoragefencelineage.ts |
Strict ObjectStorage provider-lineage authorities, resource identities, token claims, and persisted-record validation. |
ts/corestore.plugins.ts |
Centralized dependency imports. |
ts/corestore.classes.controlauth.ts |
Strict bearer-token and token-file validation. |
ts/corestore.classes.restoregrant.ts |
Signed isolated-restore grant verification and node binding. |
ts/corestore.classes.isolatedrestorecoordinator.ts |
Durable leases, fencing, receipts, reconciliation, and crash recovery. |
ts/corestore.classes.safetar.ts |
Bounded fail-closed archive extraction. |
ts_migration/runner.ts |
Ordered, idempotent persisted-data migrations run at start-up, with completion recorded in the control database. |
ts_migration/step.volumearchiveformat.ts |
The 34.0.0 conversion of the volume archive to containerarchive index generations, keeping volume-archive.pre-34. |
ts/corestore.startupnotifier.ts |
Start-up progress for the service manager: timeout extensions only while a data-proportional step advances, a fresh start budget once it finishes, STATUS= progress and the named failure. |
binary/corestore.ts |
Entry point of the self-contained host executable: engine paths beside it, systemd notifications (READY=1, start-up progress) and the offline operator commands (runHostExecutable). |
scripts/host-package.mjs |
Selects and verifies the host bundle inputs for tspack. |
ts_migration/legacy-mongodb-to-smartdb.ts |
Explicit offline MongoDB-wire to NoSQLDB migration with compatibility preflight, publication fencing, and exact verification. |
migration.js |
Operator-only migration entrypoint included in the container image and package. |
ts_migration/legacy-resource-adoption.ts |
The offline legacy resource adoption and its operator command, run by migration.js and the host executable. |
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in license.md.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.