@everystack/cli
v0.2.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
});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.
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 productionDatabase Management
These commands invoke your Lambda function directly via IAM (no database credentials exposed):
everystack db:migrate # Run Drizzle migrations
everystack db:seed # Seed database (dev only)
everystack db:psql --stage dev # Open a psql session via Lambdadb:migrate and db:seed dispatch to your Lambda's onAction handler. db:psql proxies a PostgreSQL session through the Lambda, so your database credentials never leave AWS.
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
MIT
