Canonical backend service for storing, validating, indexing, and distributing Ownables on Base-focused EVM networks.
The Hub is not just a file host. It accepts uploaded Ownable archives, validates them against anchored and public event history, stores package and chain data separately, derives recipient availability, exposes that availability to wallets for discovery and import, and returns indexed event data so clients can verify Ownables independently.
- Accepts uploaded Ownable archives and validates them before making them available
- Stores package artifacts and canonical Ownable chain data separately
- Indexes relevant Anchor-contract EVM events for server-side and wallet-side verification
- Tracks and exposes public events needed to replay and verify Ownable state
- Derives which owner account an Ownable is currently available to
- Exposes Hub-backed recipient discovery for wallet import flows
- Serves Ownable bundle downloads, including
chain.json, and indexed event history
The current Hub model is built around validation and discovery rather than messaging.
- Sender-side transfer appends the private transfer event locally, then uploads the resulting archive to Hub
- Hub validates and replays the uploaded Ownable instead of writing private events itself
- Hub derives recipient availability from the resulting Ownable state
- Recipient wallets discover available Ownables directly from the Hub
- Wallets can fetch indexed anchor/public events from the Hub to perform their own verification
The Hub is intentionally not:
- a generic mailbox between wallets
- a notification product
- a pure file server
- a service that writes private Ownable history on behalf of wallets
The Hub is a verification boundary as much as a storage boundary.
It persistently indexes the relevant EVM events emitted through the Anchor contract. Those indexed events are used internally to validate uploaded Ownables against anchored history, and are also exposed so SDK wallets can independently verify that an Ownable's chain is consistent with the same anchor history.
In addition to anchor events, the Hub tracks and exposes public events. Together, the Ownable's chain, indexed anchor events, and public events provide the replay inputs needed to determine effective Ownable state, detect stale uploads, derive the current owner, and support import flows.
The Hub keeps package identity and Ownable identity distinct.
- Package CID identifies a specific package artifact
- Ownable identity identifies the Ownable instance
- Recipient availability and transfer semantics are keyed by Ownable identity, not only by package CID
This distinction is important for discovery, replay, and import, especially once multiple artifacts or versions are involved.
yarn installBuilt with NestJS.
The Hub consumes the breaking class-based Ownables API through Nest injection. Package CID calculation remains
a direct calculateOwnablePackageCid utility call from @ownables/core/utils; anchor validation and public-event
replay are Nest-managed service instances used by OwnableReplayService to create the per-archive runtime graph.
Legacy root function imports and positional OwnableService construction are intentionally unsupported.
Protected hub routes use SIWE-authenticated request context.
POST /api/v1/auth/noncereturns noncePOST /api/v1/auth/verifyverifies SIWE payload and returns bearer token- Send
Authorization: Bearer <base64-json-token>for protected endpoints
Config:
SIWE_DOMAIN=<expected-domain>sets accepted SIWE domain
Operational routes:
GET /redirect to API documentationGET /infobasic service metadataGET /healthreadiness and dependency health
Auth routes:
POST /api/v1/auth/noncegenerate SIWE noncePOST /api/v1/auth/verifyverify SIWE payload and return bearer token
Ownable routes:
POST /ownablesupload an Ownable package in multipart fieldfile. A newly available upload returns201, an identical available retry returns200, and an upload awaiting indexed anchor evidence returns202with its expiry. The compressed upload limit is 268,435,456 bytes.GET /ownables/:id/bundledownload the stored Ownable bundle. It contains the canonicalchain.json.GET /ownables/:id/verificationread the Hub verification report.GET /ownables/:id/public-eventsread indexed public events for one Ownable.
Resource collection routes:
GET /accounts/:account/available-ownableslist local/dev-only Hub-available Ownables for one CAIP-10 account when explicitly enabled.GET /accounts/:account/available-ownables/eventsreceive liveavailable-ownableSSE events for that account.GET /public-events?ownableId=<id>&ownableId=<id>&fromBlock=<block>receive indexedpublic-eventSSE events. One or moreownableIdvalues are required andfromBlockdefaults to0.GET /hub/wallet/:networkread the public Hub signer address and native balance foreip155:ethereum,eip155:arbitrum,eip155:polygon, oreip155:base.
Swagger: http://localhost:3000/api-docs
Runtime requires DATABASE_URL and does not fall back to libpq defaults.
Runtime storage uses OWNABLES_STORAGE if set, and defaults to file://storage when unset.
Use the local dev startup flow:
yarn db:startyarn db:start is rerunnable and will:
- create or start a local Docker Postgres container
- keep data in a persistent Docker volume
- run
yarn db:migrate:upagainstDATABASE_URL - print the exact local
DATABASE_URLused for migration
Default local settings (override with env vars):
HUB_DB_CONTAINER=ownables-hub-postgresHUB_DB_VOLUME=ownables-hub-postgres-dataHUB_DB_PORT=54329HUB_DB_NAME=ownables_hubHUB_DB_USER=ownablesHUB_DB_PASSWORD=ownablesHUB_LOCAL_DATABASE_URLcan override the derived local DSN used bydb:start- derived default
DATABASE_URL=postgres://ownables:ownables@127.0.0.1:54329/ownables_hub HUB_DB_READY_TIMEOUT_SECONDSreadiness timeout for existing containers (default60)HUB_DB_COLD_START_TIMEOUT_SECONDSreadiness timeout for new containers (default240)
Run migrations directly (for existing Postgres targets):
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/db yarn db:migrate:up
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/db yarn db:migrate:downyarn db:start only boots Postgres and runs migrations. Start the app with runtime env vars:
DATABASE_URL=postgres://ownables:ownables@127.0.0.1:54329/ownables_hub \
OWNABLES_STORAGE=file://storage \
CORS_ORIGINS=http://127.0.0.1:5173,http://localhost:5173 \
yarn start:devThe runtime targets Node 24. yarn start and yarn start:dev use the Hub Nest CLI project, with watch mode for start:dev. yarn start:prod runs dist/main.js, and yarn indexer:run runs dist/indexer/main.js. Each startup command loads .env when it exists. For local browser integration, point the SDK at the Hub origin through VITE_HUB and optionally enable recipient discovery with LOCAL_DEV_RECIPIENT_DISCOVERY_ENABLED=true.
You can also copy .env.example to .env for the local runtime.
Set these env vars for local runtime:
DATABASE_URLOWNABLES_STORAGE(optional; defaults tofile://storage, set to override)QUARANTINE_STORAGE(optional outside staging and production; use a separate privatefile://path ors3://bucket/prefix)CORS_ORIGINS(optional; comma-separated browser origins allowed to call Hub. Outside production the default allowlist ishttp://127.0.0.1:5173,http://localhost:5173for local SDK smoke; production defaults to no allowed origins until explicitly configured.)
Additional runtime configuration (feature-dependent):
SIGNER_PRIVATE_KEYorSIGNER_MNEMONICHUB_NETWORK_PROFILE(testnetdefault, ormainnet)TESTNET_*_RPC_URL/MAINNET_*_RPC_URLoverridesSIWE_DOMAINPUBLIC_BASE_URLfor absolute package and chain URLs in Hub responsesLOCAL_DEV_RECIPIENT_DISCOVERY_ENABLED=trueto exposeGET /accounts/:account/available-ownablesoutside production for localhost receive/import testing
Staging and production also require explicit UPLOAD_INGRESS_CONCURRENCY, UPLOAD_RATE_LIMIT_WINDOW_SECONDS, UPLOAD_RATE_LIMIT_MAX_REQUESTS, UPLOAD_PROCESSING_CONCURRENCY, UPLOAD_MAX_EXPANDED_PROCESSING_BYTES, QUARANTINE_MAX_PENDING_OBJECTS, QUARANTINE_MAX_PENDING_BYTES, QUARANTINE_RETENTION_SECONDS, QUARANTINE_PROMOTION_BATCH_SIZE, QUARANTINE_CLEANUP_BATCH_SIZE, QUARANTINE_LEASE_SECONDS, and QUARANTINE_ORPHAN_GRACE_SECONDS. These budgets are shared across Hub processes through PostgreSQL.
yarn test
yarn test:covUseful verification commands:
yarn typecheck
yarn lint:check
yarn test:e2eyarn test:e2e builds both Nest bundles and runs the native compiled-process tests. It covers core package compatibility, the generated OpenAPI route inventory, /info, /health, canonical uploads, and rejected unsupported-runtime uploads without storage writes.
Releases are created by GitHub Actions using semantic-release.
- See RELEASING.md for the release flow and commit conventions.
