OmnyStore
A complete release management and software distribution platform in pure Dart.
Organizations, projects, packages, releases, artifacts and channels behind one SDK — with a REST API server, a web-compatible client, a resumable download manager, an update service, a full CLI, and a Hub/Node architecture for distributing storage across machines.
┌─────────────── OmnyStoreHub ────────────────┐
clients ──REST──► │ discovery + routing: org → providers │
CI ──REST──► │ │
updaters──REST──► │ acme ─► node-eu, node-us │
│ globex ─► node-eu │
│ * ─► hub-local (hub as provider) │
└──────────┬──────────────────────────────────┘
│ outbound WebSocket control plane
┌───────────────┼───────────────┐
node-eu node-us hub-local
(S3, eu-west-1) (local disk) (in-process)
final store = OmnyStore(
repositories: await JsonFileRepositories.open('/var/lib/omnystore/metadata'),
storage: LocalObjectStorage('/var/lib/omnystore/objects'),
);
final release = await store.publishRelease(
packageReference: 'omnyagent',
version: Version.parse('1.2.0-beta.3'), // channel is derived: beta
);
await store.attachAsset(
releaseId: release.id,
name: 'omnyagent-linux-x64.tar.gz',
data: File('build/omnyagent-linux-x64.tar.gz').openRead(),
platform: 'linux-x64',
);
await store.latestBeta('omnyagent'); // => 1.2.0-beta.3
await store.latestRelease('omnyagent'); // => 1.1.0 — betas are never offered
// to stable subscribers
omnystore server --data /var/lib/omnystore --port 8080
API Documentation
See the full API docs at pub.dev/documentation/omnystore.
Why this exists
Shipping software means answering four questions over and over: what is the
newest build, is it right for this machine, where do I get it, and are these the
bytes you meant? Most projects answer them with a directory of tarballs, a
hand-maintained latest.json, and hope.
OmnyStore answers them as a system:
- The channel is the version.
1.2.0-beta.3is a beta; there is no separate flag that can drift out of sync with it, and a stable subscriber can never be handed a pre-release by accident. - Checksums are not optional. Computed while streaming on upload, verified on download, and a mismatch deletes the file rather than returning it. An update mechanism that hands over unverified bytes is a malware delivery channel for anyone who can interpose.
- Ranged and resumable. A 4 GB installer interrupted at 90% costs 400 MB to finish, not 4 GB.
- Storage is pluggable, and that decides your ceiling. With S3 or GCS behind it, downloads redirect straight to the bucket and no artifact byte crosses the registry.
Features
- The whole lifecycle. Organizations → projects → packages → releases → artifacts, with drafts, yanking, promotion between channels, and download analytics. Releases are immutable once published; retracting one is a yank, which stops it being offered without breaking clients that pinned it.
- Channels derived from semver.
dev,beta,release, decided by the version's pre-release tag. Queries are inclusive downward in stability, so publishing1.2.0reaches beta and stable subscribers without republishing per channel.latestRelease(),latestBeta(),latestDev(),latestChannel(),latestAny(). - Four object-storage backends. In-memory, a local directory (atomic
writes, checksum sidecars, crash-safe temp sweeping), AWS S3 (and any
S3-compatible service — MinIO, R2, B2, Ceph) with hand-implemented SigV4
presigning, and Google Cloud Storage with V4 URL signing from a service
account. All behind one small
ObjectStoragecontract you can implement yourself. - Hub/Node architecture. A hub is the discovery point; nodes hold the metadata and the bytes for the organizations they serve and dial the hub outbound, so they work behind NAT. A hub can also be a provider, so a small deployment is one process. Organizations and nodes are many to many.
- Three data planes, chosen automatically. Presigned URLs when the backend can issue them, a direct redirect when the node is reachable, and a chunked relay over the control channel when it is not — so even a fully firewalled node is a usable provider.
- REST API on OmnyHub.
/api/v1plus an unversioned/health, with CORS, optional TLS (static or automatic Let's Encrypt), pluggable authentication, and the node control endpoint — all on one port. Mountable into an OmnyHub application you already run. - Web-compatible client SDK. No
dart:ioanywhere inomnystore_client.dart; it compiles to JavaScript and runs in a browser. It implements the same interface as the embedded store, and rebuilds the server's typed exceptions from the wire — soon ReleaseNotFoundExceptionworks over HTTP exactly as it does in-process. - Update service.
UpdateCheckeranswers "is there a newer version for me", matching the client's platform, never offering a downgrade, and distinguishing "an update exists" from "an update exists that you can install". - Download manager. Streams to disk, resumes with
Range, retries a dropped transfer, verifies the whole file — including the bytes that were already there — and deletes anything that fails. - A CLI that is a library. Every command runs in-process with injected
arguments, environment and output sinks, so the whole surface is testable
without spawning a subprocess. Machine-readable
--jsonon every command; meaningful exit codes. - Strong typing, no global state. Immutable models with value equality,
sealed exception hierarchies,
abstract interface classports, and injectedClock/IdGenerator/Loggerso tests are deterministic and two stores in one process share nothing. - Tested. 411 tests: unit, integration over real sockets, the federation protocol end to end, AWS's own published SigV4 vectors, and real RSA signature verification.
Concepts
| Term | Meaning |
|---|---|
| Organization | The top-level tenant, and the unit of federation — nodes serve organizations. |
| Project | A product within an organization; groups packages released together. |
| Package | The thing that has versions, and what a client asks about. |
| Release | A published version. Immutable. Its channel comes from its version. |
| Asset | A downloadable artifact attached to a release, with a mandatory SHA-256. |
| Channel | dev / beta / release, derived from the version's pre-release tag. |
| Hub | The discovery point: routing table, aggregation, REST API, node endpoint. |
| Node | A storage provider serving one or more organizations, dialling the hub outbound. |
| Provider | Anything that can hold and serve an organization's releases — a node, or the hub itself. |
| ObjectStorage | The pluggable backend where artifact bytes actually live. |
Getting started
dependencies:
omnystore: ^1.0.0
Embedded — no server
import 'package:omnystore/omnystore.dart';
final store = OmnyStore(
repositories: MemoryRepositories(),
storage: MemoryObjectStorage(),
);
final org = await store.createOrganization(name: 'acme');
final project = await store.createProject(
organizationId: org.id, name: 'agent',
);
final package = await store.createPackage(
projectId: project.id, name: 'omnyagent',
);
final release = await store.publishRelease(
packageReference: package.id,
version: Version.parse('1.0.0'),
);
await store.attachAsset(
releaseId: release.id,
name: 'omnyagent-linux-x64.tar.gz',
data: Stream.value(bytes),
platform: 'linux-x64',
);
As a server
dart pub global activate omnystore
omnystore server --data /var/lib/omnystore --port 8080
curl localhost:8080/health
curl localhost:8080/api/v1/packages/omnyagent/releases/latest
curl -L -o agent.tar.gz localhost:8080/api/v1/assets/<id>/download
Locking it down before it is publicly reachable:
omnystore server --data /var/lib/omnystore \
--publish-token "$PUBLISH_TOKEN" \
--node-token "$NODE_TOKEN" \
--cors 'https://releases.example.com' \
--tls-cert /etc/tls/fullchain.pem --tls-key /etc/tls/privkey.pem
Reads stay open with a publish token set — that is the point of a distribution platform — while publishing and node registration require credentials.
From an application
import 'package:omnystore/omnystore_client.dart';
final client = OmnyStoreClient(baseUrl: 'https://store.example.com');
final update = await client.checkForUpdates(
packageReference: 'omnyagent',
currentVersion: Version.parse(myVersion),
channel: ReleaseChannel.beta,
platform: Platforms.current, // 'macos-arm64' on Apple Silicon
);
if (update.isInstallable) {
await DownloadManager().downloadAsset(
asset: update.asset!,
url: client.assetDownloadUrl(update.asset!.id),
destination: '/opt/omnyagent',
onProgress: (p) => stdout.write('\r${p.percent}%'),
);
}
isInstallable rather than updateAvailable: an update that ships no artifact
for this platform is a real state, and a download button gated on the wrong one
offers a dead end.
One release carries every architecture — macos-x64 and macos-arm64 are two
artifacts on the same 1.4.0, not two releases. Platforms.current reports
what this process is running on, and the match is exact: a client is never
handed a build for another architecture, because an Intel binary on Apple
Silicon fails at launch, on the user's machine. The CLI defaults to it too:
omnystore download --package omnyagent -o /opt # picks this machine's build
Channels
A release's channel is derived from its version, so the two can never disagree:
| Version | Channel |
|---|---|
1.0.0 |
release |
2.0.0+build5 |
release — build metadata never changes the channel |
1.1.0-beta.2 |
beta |
1.1.0-dev.3 |
dev |
1.1.0-rc.1 |
dev — an unrecognised pre-release is the least stable thing it could be |
Queries are inclusive downward in stability. A beta subscriber accepts
beta and stable but never dev; a release subscriber accepts only stable:
await store.latestChannel('omnyagent', ReleaseChannel.beta);
// the newest of {beta, release} — so a newer stable release reaches
// beta subscribers without being republished
await store.latestChannel('omnyagent', ReleaseChannel.beta, exact: true);
// strictly the newest beta
Promotion publishes a new version carrying the target channel's tag and copies the artifacts across, leaving the pre-release in place for anyone still on it:
await store.promoteRelease(betaRelease.id, ReleaseChannel.release);
// 1.2.0-beta.3 → 1.2.0
Storage backends
| Backend | Bytes live in | Presigned URLs | Download path |
|---|---|---|---|
MemoryObjectStorage |
the heap | no | through the server |
LocalObjectStorage |
a directory | no | through the server |
S3ObjectStorage |
an S3 bucket | yes | client → bucket |
GcsObjectStorage |
a GCS bucket | with a service-account key | client → bucket |
S3ObjectStorage(
bucket: 'acme-releases',
region: 'eu-west-1',
credentials: EnvironmentAwsCredentialsProvider(Platform.environment),
);
GcsObjectStorage(
bucket: 'acme-releases',
credentials: GcpServiceAccountCredentials.fromJsonString(
File('service-account.json').readAsStringSync(),
),
);
With presigning, the hub answers a download with a 302 and never sees the
artifact — which is what stops the registry's bandwidth from becoming the
distribution platform's ceiling. Everything above the storage layer is
unchanged either way.
Implementing your own is five rules and one small interface — see
example/storage/custom_storage_backend.dart.
Hub and nodes
A hub is the discovery point. Nodes hold their organizations' releases and dial the hub outbound over a WebSocket, so a node can sit behind NAT, on a private network, or in a different cloud, and still be part of a public registry.
# The hub — also a provider, so no separate node is needed beside it.
omnystore server --data /var/lib/omnystore --node-token "$NODE_TOKEN"
# A node serving two organizations.
omnystore node \
--hub wss://store.example.com/_node \
--id node-eu --token "$NODE_TOKEN" \
--org acme --org globex \
--label region=eu --priority 10 \
--data /var/lib/omnystore-node
Organizations and nodes are many to many: one organization's releases may live on several nodes, and one node may serve several organizations.
The routing rules, in one place:
- Writes follow ownership. Creating a project goes to whichever provider holds its organization; publishing a release goes to whichever holds its package. A write never fans out, so ids stay unique.
- Reads by id fan out, first hit wins, and the answer is cached.
- Listings aggregate across every provider and deduplicate by natural key
(
acme/agent/omnyagent) rather than by id — because each provider mints its own ids, so replication stays invisible to clients. - Downloads prefer a redirect, falling back to streaming.
Retiring a node is omnystore node ... then drain(): new placements stop
while downloads already in flight finish.
CLI
omnystore server Run the REST API server and the node endpoint
omnystore node Run a storage node against a hub
omnystore org create · list · delete
omnystore project create · list · delete
omnystore package create · list · delete
omnystore release publish · list · latest · promote · yank · delete
omnystore asset upload · list · delete
omnystore download Fetch an artifact, verified
omnystore check-update Exit 0 if current, 10 if an update exists
omnystore providers Show the hub and its nodes
Every command works against a remote server (--server, $OMNYSTORE_URL) or a
local directory (--data, $OMNYSTORE_DATA) — the same code path drives both,
so a workflow developed locally runs unchanged against production. --json
makes every command machine-readable.
omnystore release publish --package omnyagent --version 1.4.0 \
--notes @CHANGELOG.md \
--asset build/omnyagent-linux-x64.tar.gz:linux-x64 \
--asset build/omnyagent-macos-arm64.tar.gz:macos-arm64
omnystore release latest --package omnyagent --channel beta
omnystore download --package omnyagent -o /opt # this machine
omnystore download --package omnyagent --platform linux-x64 -o /opt
omnystore download --package omnyagent --platform all -o dist/ # every build
See doc/cli.md for the full reference.
Architecture
CLI Client SDK REST API (OmnyHub)
└──────────────┴──────────────────┘
│
┌───────┴────────┐
│ OmnyStoreApi │ one interface, three implementations
└───────┬────────┘
┌──────────┼─────────────┐
OmnyStore OmnyStoreHub OmnyStoreClient
(local) (federating) (remote, web-safe)
│ │
│ ProviderRegistry ──► RemoteNodeStoreProvider ──► node
│
┌────┴────────────────────┐
│ StoreRepositories │ metadata: memory, JSON files, your adapter
│ ObjectStorage │ bytes: memory, local, S3, GCS, yours
└─────────────────────────┘
Clean architecture throughout: abstract interface class ports, dependencies
injected at construction, no global state anywhere. Business rules — name
validation, referential integrity, release immutability, cascade deletes — live
in the service layer, so they hold identically whichever adapter is plugged in.
See doc/architecture.md.
Documentation
| Document | What it covers |
|---|---|
| doc/architecture.md | Layers, ports, the shared OmnyStoreApi, and why each seam is where it is |
| doc/api.md | Every REST endpoint, request and response shape, and the error envelope |
| doc/cli.md | Every command, flag, environment variable and exit code |
| doc/client.md | The client SDK, including browser use and error handling |
| doc/federation.md | Hub/Node: routing, placement, replication, the control protocol |
| doc/storage.md | The storage backends, their trade-offs, and writing your own |
| doc/workflows.md | Release and update workflows end to end, with CI recipes |
| example/ | Twenty runnable examples |
Testing
dart test
dart test --tags server # only the tests that bind a real port
The in-memory adapters are not mocks — they implement the same contracts as the
production ones, and the storage contract suite runs against every backend. With
an injected Clock and IdGenerator there is no wall clock and no randomness,
so assertions are exact:
final store = OmnyStore(
repositories: MemoryRepositories(),
storage: MemoryObjectStorage(),
clock: FixedClock(),
idGenerator: SequentialIdGenerator(),
);
Contributing
Bug reports and pull requests are welcome on GitHub.
Every change should keep dart analyze --fatal-infos --fatal-warnings,
dart format and dart test clean; new behaviour needs a test.
Author
Graciliano M. Passos: gmpassos@GitHub.
License
Libraries
- omnystore
- OmnyStore — a complete release management and software distribution platform in pure Dart.
- omnystore_cli
- The
omnystorecommand set, exposed as a library. - omnystore_client
- OmnyStore client SDK — web-compatible, for apps that talk to a remote registry.
- omnystore_hub
- OmnyStore Hub — the discovery point that federates storage nodes and serves the REST API.
- omnystore_node
- OmnyStore Node — a storage provider that dials a hub and serves its organizations' releases through it.