@everystack/mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/package.json +39 -0
- package/src/index.ts +58 -0
- package/src/prompts/add-feature.ts +163 -0
- package/src/prompts/debug.ts +136 -0
- package/src/prompts/deploy.ts +131 -0
- package/src/prompts/design-schema.ts +104 -0
- package/src/prompts/index.ts +16 -0
- package/src/prompts/new-app.ts +211 -0
- package/src/prompts/secure.ts +231 -0
- package/src/resources/adding-database.md +169 -0
- package/src/resources/admin.md +81 -0
- package/src/resources/auth.md +115 -0
- package/src/resources/aws-setup.md +173 -0
- package/src/resources/cli.md +108 -0
- package/src/resources/client-api.md +145 -0
- package/src/resources/core.md +196 -0
- package/src/resources/deployment.md +146 -0
- package/src/resources/events.md +87 -0
- package/src/resources/first-run.md +100 -0
- package/src/resources/getting-started.md +75 -0
- package/src/resources/handler-options.md +114 -0
- package/src/resources/images.md +73 -0
- package/src/resources/index.ts +224 -0
- package/src/resources/jobs.md +97 -0
- package/src/resources/logging.md +91 -0
- package/src/resources/plugins.md +68 -0
- package/src/resources/project-claude-md.md +127 -0
- package/src/resources/query-protocol.md +129 -0
- package/src/resources/schema-patterns.md +167 -0
- package/src/resources/security-device.md +99 -0
- package/src/resources/security.md +270 -0
- package/src/resources/ssr.md +82 -0
- package/src/resources/storage.md +63 -0
- package/src/resources/testing.md +118 -0
- package/src/tools/check-environment.ts +319 -0
- package/src/tools/index.ts +58 -0
- package/src/tools/project-status.ts +183 -0
- package/src/tools/project-validate.ts +369 -0
- package/src/tools/schema-analyze.ts +410 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Client API
|
|
2
|
+
|
|
3
|
+
> Typed query builder for everystack PostgREST APIs. Import from `@everystack/api/client`.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
Read this when building client-side data fetching in React Native, Expo, or web apps.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { createClient } from '@everystack/api/client';
|
|
12
|
+
|
|
13
|
+
const api = createClient({
|
|
14
|
+
baseUrl: '/api',
|
|
15
|
+
getToken: () => localStorage.getItem('everystack-token'),
|
|
16
|
+
onTokenExpired: async () => {
|
|
17
|
+
const res = await fetch('/api/auth/refresh', {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ refreshToken: localStorage.getItem('everystack-refresh') }),
|
|
21
|
+
});
|
|
22
|
+
const { token } = await res.json();
|
|
23
|
+
localStorage.setItem('everystack-token', token);
|
|
24
|
+
return token;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Options
|
|
30
|
+
|
|
31
|
+
| Option | Type | Description |
|
|
32
|
+
|--------|------|-------------|
|
|
33
|
+
| `baseUrl` | `string` | API base URL (e.g., `/api` or `https://api.example.com`) |
|
|
34
|
+
| `getToken` | `() => string \| null` | Returns current JWT token |
|
|
35
|
+
| `onTokenExpired` | `() => Promise<string>` | Called on 401 to refresh token. Concurrent 401s deduplicated (one refresh per batch) |
|
|
36
|
+
| `headers` | `Record<string, string>` | Additional headers for every request |
|
|
37
|
+
|
|
38
|
+
## Queries (GET)
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// Basic query
|
|
42
|
+
const { data, error } = await api.from('posts').execute();
|
|
43
|
+
|
|
44
|
+
// Filters
|
|
45
|
+
const { data } = await api.from('posts')
|
|
46
|
+
.eq('status', 'published')
|
|
47
|
+
.neq('authorId', blockedUser)
|
|
48
|
+
.gt('createdAt', yesterday)
|
|
49
|
+
.order('createdAt', 'desc')
|
|
50
|
+
.limit(10)
|
|
51
|
+
.offset(20)
|
|
52
|
+
.execute();
|
|
53
|
+
|
|
54
|
+
// Column selection
|
|
55
|
+
const { data } = await api.from('posts').select('id,title,body').execute();
|
|
56
|
+
|
|
57
|
+
// Relation embedding
|
|
58
|
+
const { data } = await api.from('posts').select('*,author(name,email)').execute();
|
|
59
|
+
|
|
60
|
+
// Logical groups
|
|
61
|
+
const { data } = await api.from('posts')
|
|
62
|
+
.or('status.eq.draft,status.eq.pending')
|
|
63
|
+
.execute();
|
|
64
|
+
|
|
65
|
+
// Exact count
|
|
66
|
+
const { data, count } = await api.from('posts')
|
|
67
|
+
.eq('status', 'published')
|
|
68
|
+
.count()
|
|
69
|
+
.execute();
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Filter Methods
|
|
73
|
+
|
|
74
|
+
| Method | Example |
|
|
75
|
+
|--------|---------|
|
|
76
|
+
| `.eq(col, val)` | `?col=eq.val` |
|
|
77
|
+
| `.neq(col, val)` | `?col=neq.val` |
|
|
78
|
+
| `.gt(col, val)` | `?col=gt.val` |
|
|
79
|
+
| `.gte(col, val)` | `?col=gte.val` |
|
|
80
|
+
| `.lt(col, val)` | `?col=lt.val` |
|
|
81
|
+
| `.lte(col, val)` | `?col=lte.val` |
|
|
82
|
+
| `.like(col, pattern)` | `?col=like.pattern` |
|
|
83
|
+
| `.ilike(col, pattern)` | `?col=ilike.pattern` |
|
|
84
|
+
| `.is(col, val)` | `?col=is.val` (null/not.null) |
|
|
85
|
+
| `.in(col, vals)` | `?col=in.(a,b,c)` |
|
|
86
|
+
| `.not(col, op, val)` | `?col=not.op.val` |
|
|
87
|
+
| `.or(expr)` | `?or=(a.eq.1,b.eq.2)` |
|
|
88
|
+
| `.select(cols)` | `?select=cols` |
|
|
89
|
+
| `.order(col, dir)` | `?order=col.dir` |
|
|
90
|
+
| `.limit(n)` | `?limit=n` |
|
|
91
|
+
| `.offset(n)` | `?offset=n` |
|
|
92
|
+
| `.count()` | Adds `Prefer: count=exact` |
|
|
93
|
+
|
|
94
|
+
## Mutations
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// Insert
|
|
98
|
+
const { data, error } = await api.from('posts')
|
|
99
|
+
.insert({ body: 'Hello world', authorId: user.sub });
|
|
100
|
+
|
|
101
|
+
// Update
|
|
102
|
+
const { data, error } = await api.from('posts')
|
|
103
|
+
.eq('id', postId)
|
|
104
|
+
.update({ body: 'Updated content' });
|
|
105
|
+
|
|
106
|
+
// Delete
|
|
107
|
+
const { data, error } = await api.from('posts')
|
|
108
|
+
.eq('id', postId)
|
|
109
|
+
.delete();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## RPC
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const { data, error } = await api.rpc('timeline', { limit: 20, offset: 0 });
|
|
116
|
+
const { data } = await api.rpc('search', { query: 'hiking trails', limit: 10 });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Response Shape
|
|
120
|
+
|
|
121
|
+
All methods return `{ data, error, count? }`:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
interface ApiResponse<T> {
|
|
125
|
+
data: T | null;
|
|
126
|
+
error: { status: number; message: string; details?: string } | null;
|
|
127
|
+
count?: number; // Only with .count()
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Token Refresh
|
|
132
|
+
|
|
133
|
+
When a request returns 401:
|
|
134
|
+
1. Client calls `onTokenExpired()` to get a new token
|
|
135
|
+
2. If multiple concurrent 401s fire, they all wait for the SAME refresh (deduplication)
|
|
136
|
+
3. Client retries the original request with the new token
|
|
137
|
+
4. If refresh fails, returns `{ data: null, error: { status: 401, ... } }`
|
|
138
|
+
|
|
139
|
+
## Gotchas
|
|
140
|
+
|
|
141
|
+
- `.execute()` is required to send the request. The builder is lazy.
|
|
142
|
+
- `.insert()`, `.update()`, `.delete()` send immediately (no `.execute()` needed).
|
|
143
|
+
- Filters on `.update()` and `.delete()` scope which rows are affected.
|
|
144
|
+
- The client auto-adds `Content-Type: application/json` for mutations.
|
|
145
|
+
- `baseUrl` should NOT include a trailing slash.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# everystack
|
|
2
|
+
|
|
3
|
+
Self-hosted application stack for Expo apps on AWS. Start with a static site, add database/auth/jobs as you grow. Each tier builds on the previous -- you never rip out what you have, you add to it.
|
|
4
|
+
|
|
5
|
+
## Tiers
|
|
6
|
+
|
|
7
|
+
### V1: Static Site (no database)
|
|
8
|
+
- Expo app exported to static assets, served from CloudFront
|
|
9
|
+
- Lambda SSR for SEO (server-rendered HTML with OG meta, JSON-LD)
|
|
10
|
+
- OTA updates via `everystack update` (instant deploys without redeploying infrastructure)
|
|
11
|
+
- **Packages:** `@everystack/server`, `@everystack/cli`, `@everystack/ui`
|
|
12
|
+
- **Infrastructure:** S3 + CloudFront + Lambda + S3 (updates bucket)
|
|
13
|
+
|
|
14
|
+
### V2: Dynamic App (database + auth)
|
|
15
|
+
- PostgreSQL via RDS Aurora Serverless
|
|
16
|
+
- PostgREST-compatible REST API (filters, pagination, relations, RPC)
|
|
17
|
+
- JWT auth (signup, signin, refresh, edge verification)
|
|
18
|
+
- Admin dashboard (config-driven, EverystackAdapter)
|
|
19
|
+
- Structured logging and analytics
|
|
20
|
+
- **Adds:** `@everystack/api`, `@everystack/auth`, `@everystack/admin`, `@everystack/logging`, `@everystack/query`, `@everystack/security`
|
|
21
|
+
- **Infrastructure:** V1 + RDS Aurora Serverless
|
|
22
|
+
|
|
23
|
+
### V3: Full Platform (jobs + media)
|
|
24
|
+
- SQS background workers with dead letter queue
|
|
25
|
+
- On-demand image resizing via Sharp on Lambda
|
|
26
|
+
- S3 file uploads with presigned URLs and CDN delivery
|
|
27
|
+
- **Adds:** `@everystack/jobs`, `@everystack/storage`, `@everystack/images`
|
|
28
|
+
- **Infrastructure:** V2 + SQS + Worker Lambda + Image Lambda + Media S3
|
|
29
|
+
|
|
30
|
+
## Package Map
|
|
31
|
+
|
|
32
|
+
| Package | Purpose |
|
|
33
|
+
|---------|---------|
|
|
34
|
+
| `@everystack/server` | Lambda handler, router, DB connection, SSR, image processing, worker, plugin system |
|
|
35
|
+
| `@everystack/api` | PostgREST-compatible handler (`createHandler`) + typed client (`createClient`) |
|
|
36
|
+
| `@everystack/auth` | JWT auth flows, OAuth, password hashing, edge verification, React AuthProvider |
|
|
37
|
+
| `@everystack/cli` | CLI binary (`everystack`), OTA updates handler, storage adapters |
|
|
38
|
+
| `@everystack/admin` | Declarative admin dashboard, EverystackAdapter, presets |
|
|
39
|
+
| `@everystack/logging` | Structured logging, crash reports, analytics, S3 storage, CloudWatch trigger |
|
|
40
|
+
| `@everystack/query` | React Query hooks for PostgREST APIs |
|
|
41
|
+
| `@everystack/security` | Device attestation (Apple/Google), biometric auth, RS256 keys |
|
|
42
|
+
| `@everystack/jobs` | SQS background workers, job dispatch, Postgres materialization |
|
|
43
|
+
| `@everystack/storage` | S3 uploads, presigned URLs, MIME validation, ownership enforcement |
|
|
44
|
+
| `@everystack/images` | Sharp image processing, variant generation, EXIF extraction |
|
|
45
|
+
| `@everystack/events` | PostgreSQL LISTEN/NOTIFY, WebSocket fan-out, Lambda event bridge |
|
|
46
|
+
| `@everystack/ui` | React Native components, charts, MDX support |
|
|
47
|
+
|
|
48
|
+
## Core Insight: Two Paths, One Database
|
|
49
|
+
|
|
50
|
+
The central design decision (V2+). SSR and mobile share the same PostgreSQL database but access it differently:
|
|
51
|
+
|
|
52
|
+
- **Mobile app** -> HTTP -> PostgREST handler -> Drizzle -> PostgreSQL
|
|
53
|
+
- **SSR loader** -> Drizzle directly -> PostgreSQL (same Lambda process, zero network overhead)
|
|
54
|
+
|
|
55
|
+
Both paths use the same Drizzle schema as source of truth. One deployment unit (single Lambda), shared connection pool, no inter-service latency.
|
|
56
|
+
|
|
57
|
+
## Project Structure Conventions
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
my-app/
|
|
61
|
+
├── app/ # Expo Router pages + API routes
|
|
62
|
+
│ ├── api/
|
|
63
|
+
│ │ └── [...path]+api.ts # Catch-all API route (mounts handler)
|
|
64
|
+
│ ├── admin/ # Admin dashboard pages
|
|
65
|
+
│ └── (tabs)/ # App screens
|
|
66
|
+
├── db/
|
|
67
|
+
│ └── schema.ts # Drizzle schema (your tables)
|
|
68
|
+
├── drizzle/ # SQL migration files (generated by drizzle-kit)
|
|
69
|
+
├── server/
|
|
70
|
+
│ ├── api.ts # Main Lambda handler (createLambdaHandler)
|
|
71
|
+
│ ├── worker.ts # Background worker handler (V3)
|
|
72
|
+
│ ├── image.ts # Image processing handler (V3)
|
|
73
|
+
│ ├── rpc/ # Custom RPC functions
|
|
74
|
+
│ └── plugins/ # Plugin compositions
|
|
75
|
+
├── lib/
|
|
76
|
+
│ ├── api.ts # Client re-exports (createClient config)
|
|
77
|
+
│ └── auth-context.tsx # Auth context provider
|
|
78
|
+
├── sst.config.ts # Infrastructure definition
|
|
79
|
+
├── app.json # Expo config
|
|
80
|
+
└── package.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Handler Quick Reference
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { createHandler } from '@everystack/api/handler';
|
|
87
|
+
|
|
88
|
+
const handler = createHandler(db, schema, {
|
|
89
|
+
basePath: '/api', // URL prefix to strip
|
|
90
|
+
auth: {
|
|
91
|
+
verifyToken: async (token) => payload, // JWT verification
|
|
92
|
+
publicRoutes: ['GET'], // Methods that skip auth
|
|
93
|
+
publicRpc: ['health'], // RPC functions that skip auth
|
|
94
|
+
roleHierarchy: ['public', 'authenticated', 'admin'],
|
|
95
|
+
},
|
|
96
|
+
pgSettings: (user, client) => ({ // RLS context injection
|
|
97
|
+
role: user?.role === 'admin' ? 'admin' : user ? 'authenticated' : 'anon',
|
|
98
|
+
'request.jwt.claims': JSON.stringify(user || { role: 'anon' }),
|
|
99
|
+
}),
|
|
100
|
+
relations: { posts: { author: { table: 'users', from: 'authorId', to: 'id' } } },
|
|
101
|
+
rpc: { timeline: { fn: async (body, user) => {}, role: 'authenticated' } },
|
|
102
|
+
exposedTables: ['posts', 'profiles'], // Whitelist (404 for others)
|
|
103
|
+
hiddenColumns: { users: ['passwordHash'] }, // Strip from responses
|
|
104
|
+
protectedFields: { profiles: ['role'] }, // Strip from writes
|
|
105
|
+
rowOwnership: { posts: { column: 'authorId', userField: 'sub' } },
|
|
106
|
+
hooks: { posts: { beforeCreate: async (body, user) => ({ ...body, authorId: user?.sub }) } },
|
|
107
|
+
softDelete: { column: 'deletedAt', tables: ['posts'] },
|
|
108
|
+
maxEmbedDepth: 3,
|
|
109
|
+
maxLimit: 1000,
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Returns `(request: Request) => Promise<Response>` -- Web Standard interface. Works with Expo Router, Cloudflare Workers, Deno, Bun.
|
|
114
|
+
|
|
115
|
+
## Lambda Handler
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { createLambdaHandler } from '@everystack/server';
|
|
119
|
+
|
|
120
|
+
export const handler = createLambdaHandler({
|
|
121
|
+
init: async () => ({ api, auth }), // Lazy init, returns handler map
|
|
122
|
+
routes: (h) => [ // Route dispatch
|
|
123
|
+
{ path: '/api/auth/signup', method: 'POST', exact: true, handler: h.signup },
|
|
124
|
+
{ path: '/api', handler: h.api }, // Catch-all for PostgREST
|
|
125
|
+
],
|
|
126
|
+
onAction: async (action, payload) => { // CLI invocations via IAM
|
|
127
|
+
if (action === 'migrate') return runMigrations();
|
|
128
|
+
if (action === 'seed') return runSeed();
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Client
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { createClient } from '@everystack/api/client';
|
|
137
|
+
|
|
138
|
+
const api = createClient({
|
|
139
|
+
baseUrl: '/api',
|
|
140
|
+
getToken: () => token,
|
|
141
|
+
onTokenExpired: async () => newToken, // Auto-refresh with deduplication
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Queries
|
|
145
|
+
const { data } = await api.from('posts').eq('status', 'published').order('createdAt', 'desc').limit(10).execute();
|
|
146
|
+
const { data } = await api.from('posts').select('*,author(email)').execute();
|
|
147
|
+
|
|
148
|
+
// Mutations
|
|
149
|
+
await api.from('posts').insert({ body: 'Hello', authorId: user.sub });
|
|
150
|
+
await api.from('posts').eq('id', 1).update({ body: 'Updated' });
|
|
151
|
+
await api.from('posts').eq('id', 1).delete();
|
|
152
|
+
|
|
153
|
+
// RPC
|
|
154
|
+
const { data } = await api.rpc('timeline', { limit: 20 });
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## CLI Commands
|
|
158
|
+
|
|
159
|
+
All infrastructure commands use **AWS IAM credentials** (not shared secrets).
|
|
160
|
+
|
|
161
|
+
| Command | What it does |
|
|
162
|
+
|---------|-------------|
|
|
163
|
+
| `everystack update --channel production` | OTA deploy (no infrastructure changes) |
|
|
164
|
+
| `everystack db:migrate` | Run Drizzle migrations via Lambda invoke |
|
|
165
|
+
| `everystack db:seed` | Seed database via Lambda invoke (dev only) |
|
|
166
|
+
| `everystack db:psql --stage dev -c "SQL"` | Execute read-only SQL via Lambda |
|
|
167
|
+
| `everystack console --stage dev` | Interactive REPL with db + schema in scope |
|
|
168
|
+
| `everystack logs:errors --stage dev` | Query recent error logs |
|
|
169
|
+
| `everystack logs:tail --stage dev` | Tail CloudWatch Lambda logs |
|
|
170
|
+
| `everystack logs:query --stage dev` | Flexible log search with filters |
|
|
171
|
+
| `everystack cache:purge` | Bust CloudFront cache via KVS |
|
|
172
|
+
| `everystack certs:generate` | Generate RSA key pair for code signing |
|
|
173
|
+
| `everystack diag URL` | Diagnose deployed page freshness |
|
|
174
|
+
| `everystack analyze:ssr` | Static analysis for SSR anti-patterns |
|
|
175
|
+
|
|
176
|
+
## Key Patterns
|
|
177
|
+
|
|
178
|
+
**Schema-agnostic.** The library knows nothing about your tables. You pass your Drizzle schema to `createHandler()`. Your schema, your migrations, your database.
|
|
179
|
+
|
|
180
|
+
**Web Standards.** Handler uses Request/Response. No Express, no Fastify. Works with any Web Standard runtime.
|
|
181
|
+
|
|
182
|
+
**SST for infrastructure.** Resource linking injects secrets at deploy time (no env vars). `_action` dispatch lets the CLI invoke Lambda directly via IAM.
|
|
183
|
+
|
|
184
|
+
**Progressive complexity.** Install only what you need. V1 has zero database code. V2 adds it. V3 adds workers. Each tier is additive.
|
|
185
|
+
|
|
186
|
+
**Plugin architecture.** Packages provide plugins (`@everystack/auth/plugin`, `@everystack/jobs/plugin`) that compose into a single Lambda handler via `createPluginLambdaHandler`.
|
|
187
|
+
|
|
188
|
+
## Conventions
|
|
189
|
+
|
|
190
|
+
- TypeScript strict mode, named exports only (no default exports)
|
|
191
|
+
- Files: `kebab-case.ts`. Types: `PascalCase`. Functions: `camelCase`
|
|
192
|
+
- Tests in `__tests__/` mirroring source, named `{feature}.test.ts`
|
|
193
|
+
- Conventional commits: `feat:`, `fix:`, `chore:`, `docs:`, `test:`
|
|
194
|
+
- TDD: write failing test first, implement, refactor
|
|
195
|
+
- Zero runtime dependencies in the API library
|
|
196
|
+
- Peer dependencies for optional integrations
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Deployment Guide
|
|
2
|
+
|
|
3
|
+
> SST deployment: infrastructure setup, secrets, stages, CloudFront configuration.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
Read this when deploying an everystack app to AWS for the first time or adding a new stage.
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
|
|
10
|
+
- Node.js 20+, pnpm
|
|
11
|
+
- AWS account with credentials (`aws configure`)
|
|
12
|
+
- SST CLI (`pnpm add -g sst`)
|
|
13
|
+
|
|
14
|
+
## SST Config
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// sst.config.ts
|
|
18
|
+
export default $config({
|
|
19
|
+
app(input) {
|
|
20
|
+
return {
|
|
21
|
+
name: 'my-app',
|
|
22
|
+
removal: input?.stage === 'production' ? 'retain' : 'remove',
|
|
23
|
+
home: 'aws',
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
async run() {
|
|
27
|
+
// V1: Static site
|
|
28
|
+
const updates = new sst.aws.Bucket('Updates');
|
|
29
|
+
|
|
30
|
+
// V2: Add database
|
|
31
|
+
const vpc = new sst.aws.Vpc('Vpc');
|
|
32
|
+
const database = new sst.aws.Postgres('Database', {
|
|
33
|
+
vpc,
|
|
34
|
+
scaling: { min: '0.5 ACU', max: '2 ACU' },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// V3: Add queues + media
|
|
38
|
+
const media = new sst.aws.Bucket('Media');
|
|
39
|
+
const dlq = new sst.aws.Queue('DeadLetterQueue');
|
|
40
|
+
const jobs = new sst.aws.Queue('Jobs', { dlq: dlq.arn });
|
|
41
|
+
|
|
42
|
+
// Lambda functions
|
|
43
|
+
const api = new sst.aws.Function('Api', {
|
|
44
|
+
handler: 'server/api.handler',
|
|
45
|
+
link: [database, updates],
|
|
46
|
+
vpc,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// V3: Worker + Image Lambdas
|
|
50
|
+
const worker = new sst.aws.Function('Worker', {
|
|
51
|
+
handler: 'server/worker.handler',
|
|
52
|
+
link: [database, jobs, media],
|
|
53
|
+
vpc,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// CDN
|
|
57
|
+
const router = new sst.aws.Router('Router', {
|
|
58
|
+
routes: { '/*': api.url },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// CLI needs these outputs
|
|
62
|
+
return {
|
|
63
|
+
routerUrl: router.url,
|
|
64
|
+
apiFunctionName: api.name,
|
|
65
|
+
updatesBucket: updates.name,
|
|
66
|
+
clientBundlesBucket: updates.name,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Secrets
|
|
73
|
+
|
|
74
|
+
SST secrets are per-stage, encrypted, stored in S3:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pnpm sst secret set JwtSecret "$(openssl rand -base64 32)" --stage dev
|
|
78
|
+
pnpm sst secret set JwtSecret "$(openssl rand -base64 64)" --stage production
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Access in code via Resource linking: `Resource.JwtSecret.value`. Never in env vars or code.
|
|
82
|
+
|
|
83
|
+
## Stages
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pnpm sst deploy --stage dev # Development
|
|
87
|
+
pnpm sst deploy --stage production # Production
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Each stage gets isolated resources (separate database, buckets, Lambda functions). Use `removal: 'retain'` for production to prevent accidental deletion.
|
|
91
|
+
|
|
92
|
+
## Deploy Flow
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 1. Export Expo app
|
|
96
|
+
pnpm export
|
|
97
|
+
|
|
98
|
+
# 2. Deploy infrastructure + code
|
|
99
|
+
pnpm sst deploy --stage dev
|
|
100
|
+
|
|
101
|
+
# 3. Run migrations
|
|
102
|
+
everystack db:migrate
|
|
103
|
+
|
|
104
|
+
# 4. Seed database (dev only)
|
|
105
|
+
everystack db:seed
|
|
106
|
+
|
|
107
|
+
# 5. Subsequent content updates (no deploy needed)
|
|
108
|
+
everystack update --channel production
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Resource Linking
|
|
112
|
+
|
|
113
|
+
SST injects resource details into Lambda at deploy time:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { Resource } from 'sst';
|
|
117
|
+
|
|
118
|
+
// Database credentials (automatic from sst.aws.Postgres)
|
|
119
|
+
Resource.Database.host
|
|
120
|
+
Resource.Database.port
|
|
121
|
+
Resource.Database.database
|
|
122
|
+
Resource.Database.username
|
|
123
|
+
Resource.Database.password
|
|
124
|
+
|
|
125
|
+
// Secrets
|
|
126
|
+
Resource.JwtSecret.value
|
|
127
|
+
|
|
128
|
+
// Buckets
|
|
129
|
+
Resource.Updates.name
|
|
130
|
+
Resource.Media.name
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
No env vars needed. `@everystack/server/db` provides `createDb()` which reads these automatically.
|
|
134
|
+
|
|
135
|
+
## AWS IAM Profiles
|
|
136
|
+
|
|
137
|
+
See everystack://security for the three-profile model (everystack-create, everystack-manage, everystack-deploy).
|
|
138
|
+
|
|
139
|
+
## Gotchas
|
|
140
|
+
|
|
141
|
+
- First deploy creates VPC + RDS (can take 10-15 minutes)
|
|
142
|
+
- Database credentials are managed by SST/RDS (no manual password management)
|
|
143
|
+
- `sst dev` provides live Lambda debugging with hot reload
|
|
144
|
+
- `removal: 'remove'` deletes all resources when the stage is removed
|
|
145
|
+
- `removal: 'retain'` keeps RDS and S3 even if the stage is removed
|
|
146
|
+
- Outputs are written to `.sst/outputs.json` (used by CLI for auto-discovery)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Events & WebSocket
|
|
2
|
+
|
|
3
|
+
> Real-time events via PostgreSQL LISTEN/NOTIFY and WebSocket fan-out. Import from `@everystack/events`.
|
|
4
|
+
|
|
5
|
+
## When to Use
|
|
6
|
+
Read this when adding real-time updates: live feeds, notifications, collaborative editing.
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
import { createEventHandler } from '@everystack/events/handler';
|
|
12
|
+
|
|
13
|
+
const events = createEventHandler({
|
|
14
|
+
db,
|
|
15
|
+
channels: ['table:posts', 'table:profiles', 'user:notifications'],
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## How It Works
|
|
20
|
+
|
|
21
|
+
1. Database change triggers NOTIFY on a channel
|
|
22
|
+
2. Listener Lambda receives the notification
|
|
23
|
+
3. Fan-out via WebSocket to connected clients
|
|
24
|
+
4. Client `useSignal` hook fires callback
|
|
25
|
+
|
|
26
|
+
## Client Hook
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { useSignal } from '@everystack/events/client';
|
|
30
|
+
|
|
31
|
+
function PostList() {
|
|
32
|
+
const [posts, setPosts] = useState(initialPosts);
|
|
33
|
+
|
|
34
|
+
useSignal('table:posts', () => {
|
|
35
|
+
// Refetch when posts table changes
|
|
36
|
+
refetchPosts().then(setPosts);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return <FlatList data={posts} />;
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Database Trigger
|
|
44
|
+
|
|
45
|
+
```sql
|
|
46
|
+
CREATE OR REPLACE FUNCTION notify_table_change()
|
|
47
|
+
RETURNS trigger AS $$
|
|
48
|
+
BEGIN
|
|
49
|
+
PERFORM pg_notify('table:' || TG_TABLE_NAME, json_build_object(
|
|
50
|
+
'operation', TG_OP,
|
|
51
|
+
'id', COALESCE(NEW.id, OLD.id)
|
|
52
|
+
)::text);
|
|
53
|
+
RETURN COALESCE(NEW, OLD);
|
|
54
|
+
END;
|
|
55
|
+
$$ LANGUAGE plpgsql;
|
|
56
|
+
|
|
57
|
+
CREATE TRIGGER posts_notify
|
|
58
|
+
AFTER INSERT OR UPDATE OR DELETE ON posts
|
|
59
|
+
FOR EACH ROW EXECUTE FUNCTION notify_table_change();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Actions
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { createEventActions } from '@everystack/events/actions';
|
|
66
|
+
|
|
67
|
+
const actions = createEventActions({
|
|
68
|
+
invokeFunction: Resource.ListenerFunction.name,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Trigger from API handler
|
|
72
|
+
await actions.notify('user:notifications', { userId: 'abc', message: 'New follower' });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Schema
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { eventsSchema } from '@everystack/events/schema';
|
|
79
|
+
// Adds: event_subscriptions table for persistent subscriptions
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Gotchas
|
|
83
|
+
|
|
84
|
+
- LISTEN/NOTIFY is per-database-connection (not per-Lambda)
|
|
85
|
+
- The listener Lambda must maintain a persistent connection
|
|
86
|
+
- WebSocket connections are managed separately from HTTP
|
|
87
|
+
- Payload size limit for NOTIFY is 8000 bytes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Running Your App Locally
|
|
2
|
+
|
|
3
|
+
You have a project. Now let's see it work.
|
|
4
|
+
|
|
5
|
+
## Starting the dev server
|
|
6
|
+
|
|
7
|
+
Run this command from your project directory:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx expo start
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or if you have pnpm installed:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What you'll see
|
|
20
|
+
|
|
21
|
+
The terminal will show output like this:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Starting Metro Bundler
|
|
25
|
+
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
|
|
26
|
+
█ QR CODE HERE █
|
|
27
|
+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
|
28
|
+
|
|
29
|
+
› Press w │ open web
|
|
30
|
+
› Press i │ open iOS simulator
|
|
31
|
+
› Press a │ open Android emulator
|
|
32
|
+
› Press r │ reload app
|
|
33
|
+
› Press j │ open debugger
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Metro Bundler** is the tool that packages your code so a browser or phone can run it. It starts automatically.
|
|
37
|
+
|
|
38
|
+
## Opening in a browser
|
|
39
|
+
|
|
40
|
+
Press **w** in the terminal. Your browser will open to something like `http://localhost:8081`. You should see your app's home screen.
|
|
41
|
+
|
|
42
|
+
If `w` doesn't work, open your browser manually and go to `http://localhost:8081`.
|
|
43
|
+
|
|
44
|
+
## Opening on your phone
|
|
45
|
+
|
|
46
|
+
Install the **Expo Go** app from the App Store (iPhone) or Google Play (Android). Then scan the QR code shown in the terminal with your phone's camera. The app will load on your phone over your local Wi-Fi network.
|
|
47
|
+
|
|
48
|
+
Your phone and computer must be on the same Wi-Fi network for this to work.
|
|
49
|
+
|
|
50
|
+
## Hot reload
|
|
51
|
+
|
|
52
|
+
When you change a file and save it, the app updates automatically. You don't need to restart anything. This is called **hot reload**.
|
|
53
|
+
|
|
54
|
+
Try it: open `app/index.tsx` in your editor, change some text, save the file, and watch the browser or phone update.
|
|
55
|
+
|
|
56
|
+
## Stopping the server
|
|
57
|
+
|
|
58
|
+
Press **Ctrl+C** in the terminal. This stops the dev server. Your app will stop working in the browser until you start it again.
|
|
59
|
+
|
|
60
|
+
## Common issues
|
|
61
|
+
|
|
62
|
+
### "Port 8081 is already in use"
|
|
63
|
+
|
|
64
|
+
Something else is using that port. Either stop the other program, or start Expo on a different port:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx expo start --port 8082
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### "Unable to resolve module"
|
|
71
|
+
|
|
72
|
+
A package is missing. Run:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pnpm install
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then try starting again.
|
|
79
|
+
|
|
80
|
+
### The app is stuck or shows an old version
|
|
81
|
+
|
|
82
|
+
Clear the Metro cache and restart:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx expo start --clear
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### "Network response timed out" on phone
|
|
89
|
+
|
|
90
|
+
Your phone and computer aren't on the same network, or a firewall is blocking the connection. Make sure both are on the same Wi-Fi.
|
|
91
|
+
|
|
92
|
+
### White screen or "Something went wrong"
|
|
93
|
+
|
|
94
|
+
Check the terminal for red error text. The error message will tell you what's wrong. Copy the error and ask Claude for help.
|
|
95
|
+
|
|
96
|
+
## What's next
|
|
97
|
+
|
|
98
|
+
Once your app runs locally and you can see changes in real time, you're ready to start building. Describe what you want to add and Claude will help you implement it.
|
|
99
|
+
|
|
100
|
+
When you're ready to put your app on the internet so others can use it, you'll set up AWS and deploy. That's a separate step — there's no rush. Build locally first, deploy when you're ready.
|