@everystack/server
v0.4.27
Published
Server runtime primitives for Lambda — event adapters, routing, SSR, image processing
Readme
@everystack/server
Lambda runtime primitives for building serverless applications with SST. Provides event adapters, routing, database connection, SSR, image processing, migrations, and background job handling.
Install
pnpm add @everystack/serverEntry Points
| Export | Purpose |
|--------|---------|
| @everystack/server | Lambda handler, router, event adapters, logging |
| @everystack/server/db | SST Resource-linked database connection |
| @everystack/server/ssr | Expo web SSR bundle serving |
| @everystack/server/image | On-demand image resizing via Sharp |
| @everystack/server/media | Fingerprinted media asset URL resolver |
| @everystack/server/migrate | Drizzle migration runner |
| @everystack/server/worker | SQS job queue consumer |
| @everystack/server/stubs | esbuild bundling helpers for client packages |
Quick Start
import { createLambdaHandler } from '@everystack/server';
import { createDb, getJwtSecret } from '@everystack/server/db';
import { runMigrations } from '@everystack/server/migrate';
import { createWorkerHandler } from '@everystack/server/worker';
import * as schema from '../db/schema';
const { db } = createDb(schema);
// API Lambda
export const handler = createLambdaHandler({
init: async () => ({
api: createApiHandler(db),
}),
routes: (h) => [
{ path: '/api', handler: h.api },
],
onAction: async (action, payload) => {
// CLI invocations via IAM-authed Lambda invoke
if (action === 'migrate') return runMigrations(db, './drizzle');
if (action === 'seed') return runSeed(db);
},
});
// Worker Lambda
export const worker = createWorkerHandler(async () => ({
'email:send': async (payload) => { /* ... */ },
'image:process': async (payload) => { /* ... */ },
}));Main Export (@everystack/server)
createLambdaHandler(options)
The primary entry point. Wraps initialization, routing, and error handling into a Lambda handler.
interface LambdaHandlerOptions {
init: () => Promise<Record<string, Handler>>;
routes: (handlers: Record<string, Handler>) => Route[];
fallback?: (handlers: Record<string, Handler>) => Handler;
onAction?: (action: string, payload: unknown, handlers: Record<string, Handler>) => Promise<unknown>;
}Behavior:
- Lazy-initializes handlers once, caches across warm invocations
- Detects CLI invocations (events with
_actionfield) and dispatches toonAction - Sets Cache-Control:
private, no-store(authenticated) orpublic, s-maxage=300(public GET) - Guards response size (5MB max for Function URLs)
- Adds
x-request-idfrom Lambda trace or UUID
createRouter(routes, fallback?)
Path/method dispatcher.
interface Route {
path: string; // URL prefix match
method?: string; // HTTP method filter (optional)
exact?: boolean; // Exact path match (default: prefix)
handler: Handler;
}Routes match by prefix unless exact: true. First match wins.
eventToRequest(event)
Converts AWS API Gateway V2 events to Web Standard Request objects.
responseToResult(response)
Converts Web Standard Response to API Gateway V2 results. Auto-detects and base64-encodes binary content.
log(level, message, meta?)
Structured JSON logging to stdout with ISO timestamp.
Database (@everystack/server/db)
SST Resource-linked database helpers. All credentials come from SST's Resource linking — no env vars needed.
createDb(schema, options?)
Lazy singleton Drizzle connection via postgres.js.
const { db } = createDb(schema, { maxConnections: 1 });Pool defaults: max 1, idle timeout 60s, connect timeout 5s, max lifetime 5min, SSL required.
getDatabaseUrl()
Returns PostgreSQL connection URL from SST Resources.
getJwtSecret()
Returns JWT secret as Uint8Array from Resource.JwtSecret.value.
Database operations (dbPlugin)
dbPlugin registers the operator actions the CLI invokes over IAM-authed Lambda invoke (no HTTP, no shared secret). Add it to the ops Lambda's plugin list; it runs on the privileged operator connection (ctx.adminDb).
import { dbPlugin } from '@everystack/server/plugin';
dbPlugin({
migrationsFolder: join(__dirname, 'drizzle'),
seed: runSeed, // optional — registers `seed` (dev only)
schemas: ['auth', 'public', 'drizzle'], // db:reset drop set (dev only)
backupBucket: Resource.Backups.name, // private bucket for logical backups
forceRlsCarveouts: ['ops.runs'], // ENABLE-not-FORCE tables db:doctor shouldn't flag
});Actions contributed (each is an everystack db:* command):
| Action | Does |
|---|---|
| migrate / seed / db:reset | run migrations, seed (dev), drop+remigrate (dev) |
| db:psql / db:query | connection info / read-only SQL via Lambda |
| db:doctor | audit: is the API connection least-privilege + RLS-subject? Flags BYPASSRLS, un-FORCEd RLS, and app-reachable tables without RLS |
| db:provision | create the least-privilege role chain on an existing DB (idempotent; roles only) |
| db:backup | dispatched to the Fargate Task lane: pg_dump -Fc → gzip → S3 (streamed), fingerprint-stamped; awaits the child exit code, deletes the partial on failure |
| db:backups | list a stage's backups |
| db:restore | S3 → gunzip → pg_restore --clean --if-exists (requires confirm:true) |
| db:authz:probe | run the CLI-built authorization red-team SQL in a rolled-back transaction |
| console / console:meta | the operator REPL (db, schema, models, auth in scope) |
Backup options
| Option | Type | Description |
|---|---|---|
| backupBucket | string | Private S3 bucket for logical backups. Pass Resource.Backups.name. Unset → db:backup/db:backups/db:restore return "backup storage not configured"; the rest of the plugin is unaffected. |
| backupRegion | string | Region for the backups bucket. Defaults to the Lambda's AWS_REGION. |
| forceRlsCarveouts | string[] | App-owned tables intentionally ENABLE-not-FORCE RLS (owner must write them), so db:doctor doesn't flag them. auth.users is always carved out. |
Logical backups need the pg_dump Lambda layer — see docs/backups.md. Credentials reach pg_dump/pg_restore as PG* env vars, never on argv.
Web SSR (@everystack/server/ssr)
Serves Expo web builds deployed via @everystack/cli.
getWebHandler(db, storage)
Returns a Request→Response handler for the latest web release.
- Queries database for latest
platform='web'release - Downloads and extracts brotli-compressed tar archive to
/tmp/web-build/ - Caches handler with 60s TTL (re-queries for new releases)
- Returns
nullif no web release exists
downloadAndExtract(storage, storagePrefix)
Downloads {prefix}/bundle.tar.br from storage, decompresses with brotli, extracts to /tmp/web-build/.
extractTar(data, destDir)
Pure JavaScript POSIX tar extractor. Required because Lambda doesn't include the tar binary.
Image Processing (@everystack/server/image)
On-demand image resizing via Sharp (provided as Lambda layer).
createImageHandler(config)
interface ImageHandlerConfig {
bucket: string; // S3 bucket name
region?: string; // AWS region (default: AWS_REGION env)
pathPrefix?: string; // URL prefix (default: '/media/')
cacheControl?: string; // Cache-Control header (default: 'public, max-age=86400, s-maxage=31536000, stale-while-revalidate=60')
validateKey?: (key: string) => boolean | Promise<boolean>; // Optional key validator
s3Cache?: { // Persist rendered images to S3 (skip Sharp on repeat requests)
enabled: boolean;
prefix?: string; // S3 key prefix (default: 'cache/')
};
observability?: boolean; // Emit cache-state + timing headers (default: true)
}URL format: /media/{key}?w=400&h=300&fit=cover&fm=webp&q=80&dpr=2
Behavior:
- Fetches original from S3, processes with Sharp, returns with cache headers
- All images get EXIF auto-rotation and format conversion (default: webp q80)
- With
s3Cacheenabled: checks S3 for a cached render before processing, writes result to S3 after first render. Subsequent requests from any CloudFront region skip Sharp entirely.
Observability headers
When observability is enabled (default), image responses carry additive,
non-sensitive headers that double as production render-time monitoring. They do
not change Cache-Control, Vary, status, or body.
| Header | Set on | Meaning |
| ---------------- | ----------------- | ------------------------------------------------ |
| x-image-cache | both image paths | hit = served from S3 render cache (no Sharp); render = freshly rendered |
| x-render-ms | render path only | ms spent in Sharp |
| x-image-ms | both image paths | total handler wall time (entry → response) |
| x-fetch-ms | both image paths | ms to GET the original/cached object from S3 |
| Server-Timing | both image paths | devtools mirror: cache;desc=…, s3;dur=…, render;dur=…, total;dur=… |
Telling the three pipeline states apart — CloudFront edge hit vs. S3
render-cache hit vs. fresh render. Read CloudFront's x-cache first:
| x-cache (CloudFront) | x-image-cache (Lambda) | meaning |
| ---------------------- | ------------------------ | ------------------------------------ |
| Hit from cloudfront | (ignore — stale) | EDGE HIT, Lambda not invoked |
| Miss from cloudfront | hit | S3 RENDER-CACHE HIT, no Sharp |
| Miss from cloudfront | render | FRESH RENDER (x-render-ms = Sharp time) |
Caveat: CloudFront caches the response including these headers and replays them on edge hits, so on an edge hit
x-image-cachereflects the original render, not this request. Thex-image-*headers are authoritative only whenx-cacheis aMiss. Always readx-cachefirst.
Set observability: false to suppress all of the above.
deleteImageCache(bucket, key, options?)
Deletes all cached renders for an image key. Call when the original is deleted or replaced.
import { deleteImageCache } from '@everystack/server/image';
const deleted = await deleteImageCache('my-bucket', 'uploads/photo.jpg');
// deleted = number of cached objects removedOptions: { region?: string; prefix?: string } (prefix defaults to 'cache/').
purgeImage(bucket, key, options?)
Delete all S3 cached renders and invalidate CloudFront cache for an image. Combines deleteImageCache with KVS path invalidation.
import { purgeImage } from '@everystack/server/image';
const { deleted, invalidated } = await purgeImage('my-bucket', 'uploads/photo.jpg', {
kvsArn: process.env.KVS_ARN,
});Options: { region?: string; prefix?: string; kvsArn?: string; pathPrefix?: string }. KVS invalidation is skipped when kvsArn is omitted.
parseParams(query)
Validates URL query params into transform options:
| Param | Range | Description |
|-------|-------|-------------|
| w | 1–4096 | Width |
| h | 1–4096 | Height |
| fit | cover, contain, fill, inside, outside | Resize fit |
| fm | webp, jpeg, png, avif | Output format |
| q | 1–100 | Quality |
| dpr | 1–3 | Device pixel ratio (multiplies w/h) |
Media Assets (@everystack/server/media)
Resolves logical media asset paths to fingerprinted CDN URLs. During everystack update, media assets are uploaded with content-hash suffixes (e.g., logo-a1b2c3d4.png) and a media-manifest.json is written to the media bucket. This module loads that manifest and resolves URLs at runtime.
createMediaResolver(config)
import { createMediaResolver } from '@everystack/server/media';
const media = createMediaResolver({
bucket: Resource.Media.name, // S3 bucket with media-manifest.json
cdnUrl: process.env.CDN_URL, // CDN base URL (no trailing slash)
ttlMs: 60_000, // Manifest cache TTL (default: 1 minute)
});resolve(logicalPath)
Async. Returns the fingerprinted CDN URL. Falls back to the logical path if the manifest is unavailable or the path isn't mapped.
const logoUrl = await media.resolve('email/logo.png');
// → "https://cdn.example.com/email/logo-a1b2c3d4.png"resolveSync(logicalPath)
Sync. Uses the cached manifest only. Returns null if no manifest has been loaded yet.
const url = media.resolveSync('og/default.png');
// → "https://cdn.example.com/og/default-f6e5d4c3.png" or nullpreload()
Loads the manifest during Lambda init. Call this in createLambdaHandler's init to amortize the S3 call across all requests.
export const handler = createLambdaHandler({
init: async () => {
await media.preload();
return { api };
},
// ...
});Caching behavior:
- Manifest cached in module scope across warm Lambda invocations
- After TTL expires, revalidates via
HeadObjectETag comparison (one cheap HEAD call) - Full re-fetch only when the manifest content changes
- S3 errors return stale cache gracefully
Migrations (@everystack/server/migrate)
runMigrations(db, migrationsFolder)
Runs Drizzle migrations from the specified folder. Called via onAction handler (CLI → IAM → Lambda invoke).
- Returns
{ success: true, message: 'Migrations applied' } - Throws on error (caller handles response)
Worker (@everystack/server/worker)
createWorkerHandler(init)
SQS Lambda handler with type-based job dispatch.
type JobHandler = (payload: any, job: { id: string; type: string }) => Promise<void>;
const worker = createWorkerHandler(async () => ({
'email:send': async (payload) => { /* ... */ },
'image:process': async (payload) => { /* ... */ },
}));- Lazy-initializes handlers, caches across warm invocations
- Parses SQS message body as
{ jobId, type, payload } - Returns
batchItemFailuresfor partial failure handling (failed messages retry)
Stubs (@everystack/server/stubs)
clientStubs
Array of client-side package names to exclude from Lambda bundles via esbuild external.
stubsDir
Path to no-op module stubs. Used with SST copyFiles to prevent "module not found" errors when server code transitively imports client packages.
// sst.config.ts
import { clientStubs, stubsDir } from '@everystack/server/stubs';
new sst.aws.Function('Api', {
handler: 'server/api.handler',
nodejs: {
esbuild: { external: clientStubs },
},
copyFiles: [{ from: stubsDir, to: 'stubs' }],
});Peer Dependencies
sst— Resource linking for credentialsdrizzle-orm— Database ORMpostgres— PostgreSQL driversharp— Image processing (optional, Lambda layer)@aws-sdk/client-s3— S3 operations (image handler)@aws-sdk/s3-request-presigner— Presigned URLs (image handler)@everystack/cli— Storage adapter (SSR handler)
Part of everystack — a self-hosted application stack for Expo apps on AWS.
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].
