@everystack/cli
v0.4.2
Published
CLI and OTA updates for Expo apps on everystack
Readme
@everystack/cli
CLI and OTA updates for Expo apps on everystack. Database management (db:migrate, db:seed), OTA publishing, Expo Updates protocol (v0/v1), pluggable storage, RSA-SHA256 code signing, and channels.
Install
pnpm add @everystack/cli drizzle-orm structured-headersFor S3 storage:
pnpm add @aws-sdk/client-s3Entry Points
| Import | Description |
|--------|-------------|
| @everystack/cli | Server: handler, storage adapters |
| @everystack/cli/client | Client: UpdatesProvider, AppStateUpdateProvider |
| @everystack/cli/schema | Drizzle tables (channels, releases, assets) |
Server: Handler
Creates a Web Standard handler implementing the Expo Updates manifest protocol:
import { createUpdatesHandler, createStorage } from '@everystack/cli';
import { db } from './db';
const storage = createStorage({
type: 'filesystem',
directory: './updates',
});
const handler = createUpdatesHandler({
db,
storage,
baseUrl: 'https://myapp.com',
basePath: '/api/updates',
defaultChannel: 'production',
auth: {
verifyToken: async (token) => verifyJWT(token),
},
privateKey: process.env.CODE_SIGNING_PRIVATE_KEY, // PEM for RSA-SHA256
});Mounting
// app/api/updates/[...path]+api.ts
export function GET(request: Request) { return handler(request); }
export function POST(request: Request) { return handler(request); }Handler Endpoints
The handler serves:
- Manifest requests — Expo Updates protocol v0/v1 manifest responses with multipart/mixed format
- Asset downloads — Binary assets from your configured storage
- Publish endpoint — Authenticated upload of new releases (used by the CLI)
Handler Options
interface UpdatesHandlerOptions {
db: DrizzleDb; // Drizzle instance
storage: StorageAdapter; // Filesystem or S3
baseUrl: string; // Public URL (for asset references in manifests)
basePath?: string; // URL prefix to strip
auth?: {
verifyToken: (token: string) => Promise<Record<string, unknown> | null>;
};
privateKey?: string; // PEM for RSA-SHA256 code signing
defaultChannel?: string; // Default channel (default: 'production')
}Storage Adapters
Filesystem
import { createStorage } from '@everystack/cli';
const storage = createStorage({
type: 'filesystem',
directory: './updates',
});S3
const storage = createStorage({
type: 's3',
bucket: 'my-updates-bucket',
region: 'us-east-1',
endpoint: 'https://s3.us-east-1.amazonaws.com', // Optional
});The S3 client is bounded by default so a single call can never hang: a
connect timeout (3s), a per-request timeout (10s), bounded retries (3 attempts),
and a capped list() (paged with a page backstop). This matters most for the
observability rollup — an unbounded client under an S3 503 storm blocks until
the Lambda's own timeout, and each hung invocation pins a concurrency slot, so
it can drain the account pool and starve the app it observes.
Tune per-field via options, or override the timeouts/retries by env var
without a code change (env is handy for turning a knob on a live Lambda):
const storage = createStorage({
type: 's3',
bucket: 'my-updates-bucket',
region: 'us-east-1',
options: {
connectionTimeoutMs: 3000, // env: EVERYSTACK_S3_CONNECT_TIMEOUT_MS
requestTimeoutMs: 10000, // env: EVERYSTACK_S3_REQUEST_TIMEOUT_MS
maxAttempts: 3, // env: EVERYSTACK_S3_MAX_ATTEMPTS
listMaxKeys: 1000, // keys per ListObjectsV2 page
listMaxPages: 1000, // hard backstop before list() truncates (warns)
},
});Precedence per field: explicit options > env var > default.
Custom Adapter
Implement the StorageAdapter interface:
interface StorageAdapter {
put(key: string, data: Buffer | Uint8Array, contentType?: string): Promise<void>;
get(key: string): Promise<{ data: Buffer; contentType: string } | null>;
exists(key: string): Promise<boolean>;
list(prefix: string): Promise<string[]>;
delete(key: string): Promise<void>;
}CLI
Publish updates, manage certificates, and channels from the command line.
Publish an Update
everystack update \
--channel production \
--message "Fix login bug" \
--platform ios # ios | android | web | all (default: all)This bundles your app, uploads assets to storage, creates a release record, and signs the manifest.
Media assets configured in app.config.js under extra.everystack.assets are fingerprinted with 8-char content hashes (e.g., logo.png → logo-a1b2c3d4.png) and synced to the media bucket. A media-manifest.json mapping logical names to fingerprinted paths is written to the bucket root. Stale fingerprinted files from previous deploys are pruned automatically.
// app.config.js
extra: {
everystack: {
assets: [
{ from: 'server/email/assets', to: 'email/', include: '*.{png,jpg}' },
{ from: 'assets/og', to: 'og/' },
],
},
}Use @everystack/server/media to resolve fingerprinted URLs at runtime in SSR, email templates, and workers.
Code Signing
Generate RSA key pair for manifest signing:
everystack certs:generate --output ./certs
# Creates ./certs/private-key.pem and ./certs/certificate.pem
everystack certs:configure --input ./certs --keyid main
# Configures your app to use the generated certificatesChannel Management
everystack channels list
everystack channels create --name staging
everystack channels create --name productionDeploy
everystack deploy --stage dev # provision infrastructure (wraps `sst deploy`)A thin wrapper over SST so you never type sst directly. Infrastructure only — migrations and bundle publishing stay explicit (db:migrate, update).
Database Management
These commands invoke your Lambda directly via IAM (no database credentials ever leave AWS):
# Migrations
everystack db:migrate # apply Drizzle migrations
everystack db:seed # seed (dev only)
everystack db:psql --stage dev [-c SQL] # interactive psql / one-off query via Lambda
# Model-driven schema (@everystack/model)
everystack db:generate # diff Models vs the live DB → next migration
everystack db:pull # introspect a live DB → field() Models (brownfield on-ramp)
everystack db:reconcile # deploy the derived layer (functions/views/matviews/triggers) from the declared descriptors in db/models — no migrations
everystack db:fingerprint # content-address the live base schema vs the Models — MATCH/MISMATCH
everystack db:sync # make the database match your checkout — state + compute, one verb (dev DBs)
everystack db:diff # the state edge between two declared states — no DB, CI-pure; reverse = computed rollback
everystack db:plan | db:apply # mint a reviewable plan against a target, apply it fingerprint-verified at both ends + the fast-forward rule (checkout must descend from the commit declaring the target's state); destructive plans: --confirm + snapshot + approver set when declared
everystack db:check # the CI gate per PR: merged declared state composes + artifacts match regeneration (+ ephemeral from-scratch build w/ a scratch PG)
everystack db:approvers # declare who can DESTROY: the stage's destructive-approver set (SSM, STS-verified at apply)
everystack db:backfill # one-shot data jobs in db/backfills/*.sql — content-hash identity, own per-DB record, never a schema side effect
everystack db:template:refresh # (re)build the dev template from the declared state + your db:seed script — never a data copy
everystack db:branch # per-branch dev DB minted from the template, keyed on the git branch (--list / --prune --confirm)
everystack db:fork # fork a deployed stage's DB into a feature stage (backup → presigned hand-off → restore); production is never a target
# Security
everystack db:doctor # is the DB least-privilege + RLS-subject?
everystack db:provision # create the least-privilege role chain (idempotent)
everystack db:authz:pull|diff|test|owner|report # the authorization contract + red-team
# Backups (see docs/backups.md)
everystack db:backup | db:backups | db:restore --from <id> --confirm # logical pg_dump → S3
everystack db:backup:download <id> # presigned URL to a dump (1h)
everystack db:snapshot | db:snapshots # physical RDS snapshotsThe full command reference — including secrets, logs:*, status, cache:purge, branches, and every flag — is in docs/cli.md.
Interactive Console
everystack console --stage dev # Connect to deployed database
everystack console --stage dev --sandbox # Sandbox mode: rolls back after each expressionA REPL connected to your deployed database with db, schema, and eq/and/or helpers in scope. Supports multiline input, persistent history, and tab completion.
Dot commands:
.tables/.schema [table]— Inspect database schema.login— Authenticate with email/password (hidden input).user— Show current auth context.logout— Clear auth context.sandbox— Toggle sandbox mode
After .login, queries run with RLS policies applied via pgSettings. The current_user variable is available in scope. Passwords are redacted from history.
Client: React Native
UpdatesProvider
Wraps your app to check for and apply OTA updates:
import { UpdatesProvider } from '@everystack/cli/client';
function App() {
return (
<UpdatesProvider
url="https://myapp.com/api/updates"
channel="production"
checkInterval={60000} // Check every 60 seconds
onUpdateAvailable={(update) => {
// Optional: prompt user or auto-apply
console.log('Update available:', update.message);
}}
onUpdateApplied={() => {
console.log('Update applied, restarting...');
}}
>
<MyApp />
</UpdatesProvider>
);
}AppStateUpdateProvider
Checks for updates when the app returns from background:
import { AppStateUpdateProvider } from '@everystack/cli/client';
function App() {
return (
<AppStateUpdateProvider url="https://myapp.com/api/updates" channel="production">
<MyApp />
</AppStateUpdateProvider>
);
}Schema
Add the updates tables to your Drizzle migrations:
import { channels, releases, assets } from '@everystack/cli/schema';Tables:
- channels — Named release channels (production, staging, etc.)
- releases — Published update bundles with metadata
- assets — Individual asset files referenced by releases
Expo Updates Protocol
The handler implements the full Expo Updates manifest protocol:
- Protocol v0: Legacy format for older Expo SDK versions
- Protocol v1: Modern multipart/mixed response format
- Code signing: RSA-SHA256 signatures on manifest directives
- Platform filtering: Serves platform-specific bundles based on request headers
- Channel routing: Multiple release channels with independent version tracks
How It Works
- The Expo app sends a manifest request with platform, runtime version, and current update ID
- The handler finds the latest release for the requested channel and platform
- If a newer release exists, it returns a signed manifest with asset URLs
- The Expo runtime downloads assets and applies the update
Peer Dependencies
| Package | Version | Required |
|---------|---------|----------|
| drizzle-orm | >=0.30.0 | Yes |
| structured-headers | ^1.0.0 | Yes (runtime dep) |
| @aws-sdk/client-s3 | >=3.0.0 | For S3 storage |
| expo-updates | >=0.25.0 | Client SDK |
| react | >=18.0.0 | Client SDK |
| react-native | >=18.0.0 | Client SDK |
License
AGPL-3.0-only © Scalable Technology, Inc.
A commercial license is available for organizations that cannot or do not wish to comply with the AGPL-3.0 terms. For commercial licensing, contact [email protected].
