mcp-subagents-opencode 1.0.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/LICENSE +21 -0
- package/README.md +602 -0
- package/build/config/timeouts.d.ts +9 -0
- package/build/config/timeouts.d.ts.map +1 -0
- package/build/config/timeouts.js +18 -0
- package/build/config/timeouts.js.map +1 -0
- package/build/helpers.d.ts +6 -0
- package/build/helpers.d.ts.map +1 -0
- package/build/helpers.js +47 -0
- package/build/helpers.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +245 -0
- package/build/index.js.map +1 -0
- package/build/models.d.ts +32 -0
- package/build/models.d.ts.map +1 -0
- package/build/models.js +58 -0
- package/build/models.js.map +1 -0
- package/build/server/register-notifications.d.ts +3 -0
- package/build/server/register-notifications.d.ts.map +1 -0
- package/build/server/register-notifications.js +77 -0
- package/build/server/register-notifications.js.map +1 -0
- package/build/server/register-resources.d.ts +3 -0
- package/build/server/register-resources.d.ts.map +1 -0
- package/build/server/register-resources.js +210 -0
- package/build/server/register-resources.js.map +1 -0
- package/build/server/register-retry-execution.d.ts +2 -0
- package/build/server/register-retry-execution.d.ts.map +1 -0
- package/build/server/register-retry-execution.js +28 -0
- package/build/server/register-retry-execution.js.map +1 -0
- package/build/server/register-tasks.d.ts +3 -0
- package/build/server/register-tasks.d.ts.map +1 -0
- package/build/server/register-tasks.js +52 -0
- package/build/server/register-tasks.js.map +1 -0
- package/build/server/register-tools.d.ts +3 -0
- package/build/server/register-tools.d.ts.map +1 -0
- package/build/server/register-tools.js +32 -0
- package/build/server/register-tools.js.map +1 -0
- package/build/server/resource-helpers.d.ts +21 -0
- package/build/server/resource-helpers.d.ts.map +1 -0
- package/build/server/resource-helpers.js +84 -0
- package/build/server/resource-helpers.js.map +1 -0
- package/build/services/account-manager.d.ts +88 -0
- package/build/services/account-manager.d.ts.map +1 -0
- package/build/services/account-manager.js +239 -0
- package/build/services/account-manager.js.map +1 -0
- package/build/services/claude-code-runner.d.ts +15 -0
- package/build/services/claude-code-runner.d.ts.map +1 -0
- package/build/services/claude-code-runner.js +475 -0
- package/build/services/claude-code-runner.js.map +1 -0
- package/build/services/client-context.d.ts +31 -0
- package/build/services/client-context.d.ts.map +1 -0
- package/build/services/client-context.js +44 -0
- package/build/services/client-context.js.map +1 -0
- package/build/services/exhaustion-fallback.d.ts +27 -0
- package/build/services/exhaustion-fallback.d.ts.map +1 -0
- package/build/services/exhaustion-fallback.js +30 -0
- package/build/services/exhaustion-fallback.js.map +1 -0
- package/build/services/fallback-orchestrator.d.ts +16 -0
- package/build/services/fallback-orchestrator.d.ts.map +1 -0
- package/build/services/fallback-orchestrator.js +48 -0
- package/build/services/fallback-orchestrator.js.map +1 -0
- package/build/services/opencode-client.d.ts +40 -0
- package/build/services/opencode-client.d.ts.map +1 -0
- package/build/services/opencode-client.js +147 -0
- package/build/services/opencode-client.js.map +1 -0
- package/build/services/opencode-spawner.d.ts +56 -0
- package/build/services/opencode-spawner.d.ts.map +1 -0
- package/build/services/opencode-spawner.js +426 -0
- package/build/services/opencode-spawner.js.map +1 -0
- package/build/services/output-file.d.ts +24 -0
- package/build/services/output-file.d.ts.map +1 -0
- package/build/services/output-file.js +90 -0
- package/build/services/output-file.js.map +1 -0
- package/build/services/progress-registry.d.ts +12 -0
- package/build/services/progress-registry.d.ts.map +1 -0
- package/build/services/progress-registry.js +97 -0
- package/build/services/progress-registry.js.map +1 -0
- package/build/services/question-registry.d.ts +79 -0
- package/build/services/question-registry.d.ts.map +1 -0
- package/build/services/question-registry.js +249 -0
- package/build/services/question-registry.js.map +1 -0
- package/build/services/retry-queue.d.ts +41 -0
- package/build/services/retry-queue.d.ts.map +1 -0
- package/build/services/retry-queue.js +195 -0
- package/build/services/retry-queue.js.map +1 -0
- package/build/services/sdk-client-manager.d.ts +149 -0
- package/build/services/sdk-client-manager.d.ts.map +1 -0
- package/build/services/sdk-client-manager.js +632 -0
- package/build/services/sdk-client-manager.js.map +1 -0
- package/build/services/sdk-session-adapter.d.ts +203 -0
- package/build/services/sdk-session-adapter.d.ts.map +1 -0
- package/build/services/sdk-session-adapter.js +1088 -0
- package/build/services/sdk-session-adapter.js.map +1 -0
- package/build/services/sdk-spawner.d.ts +42 -0
- package/build/services/sdk-spawner.d.ts.map +1 -0
- package/build/services/sdk-spawner.js +488 -0
- package/build/services/sdk-spawner.js.map +1 -0
- package/build/services/session-hooks.d.ts +24 -0
- package/build/services/session-hooks.d.ts.map +1 -0
- package/build/services/session-hooks.js +130 -0
- package/build/services/session-hooks.js.map +1 -0
- package/build/services/session-snapshot.d.ts +19 -0
- package/build/services/session-snapshot.d.ts.map +1 -0
- package/build/services/session-snapshot.js +203 -0
- package/build/services/session-snapshot.js.map +1 -0
- package/build/services/subscription-registry.d.ts +12 -0
- package/build/services/subscription-registry.d.ts.map +1 -0
- package/build/services/subscription-registry.js +27 -0
- package/build/services/subscription-registry.js.map +1 -0
- package/build/services/task-manager.d.ts +150 -0
- package/build/services/task-manager.d.ts.map +1 -0
- package/build/services/task-manager.js +765 -0
- package/build/services/task-manager.js.map +1 -0
- package/build/services/task-persistence.d.ts +29 -0
- package/build/services/task-persistence.d.ts.map +1 -0
- package/build/services/task-persistence.js +159 -0
- package/build/services/task-persistence.js.map +1 -0
- package/build/services/task-status-mapper.d.ts +21 -0
- package/build/services/task-status-mapper.d.ts.map +1 -0
- package/build/services/task-status-mapper.js +171 -0
- package/build/services/task-status-mapper.js.map +1 -0
- package/build/templates/index.d.ts +22 -0
- package/build/templates/index.d.ts.map +1 -0
- package/build/templates/index.js +147 -0
- package/build/templates/index.js.map +1 -0
- package/build/templates/overlays/coder-csharp.mdx +58 -0
- package/build/templates/overlays/coder-go.mdx +53 -0
- package/build/templates/overlays/coder-java.mdx +54 -0
- package/build/templates/overlays/coder-kotlin.mdx +56 -0
- package/build/templates/overlays/coder-nextjs.mdx +65 -0
- package/build/templates/overlays/coder-python.mdx +53 -0
- package/build/templates/overlays/coder-react.mdx +55 -0
- package/build/templates/overlays/coder-ruby.mdx +59 -0
- package/build/templates/overlays/coder-rust.mdx +48 -0
- package/build/templates/overlays/coder-supabase.mdx +268 -0
- package/build/templates/overlays/coder-supastarter.mdx +313 -0
- package/build/templates/overlays/coder-swift.mdx +56 -0
- package/build/templates/overlays/coder-tauri.mdx +566 -0
- package/build/templates/overlays/coder-triggerdev.mdx +296 -0
- package/build/templates/overlays/coder-typescript.mdx +45 -0
- package/build/templates/overlays/coder-vue.mdx +62 -0
- package/build/templates/overlays/planner-architecture.mdx +78 -0
- package/build/templates/overlays/planner-bugfix.mdx +36 -0
- package/build/templates/overlays/planner-feature.mdx +38 -0
- package/build/templates/overlays/planner-migration.mdx +50 -0
- package/build/templates/overlays/planner-refactor.mdx +57 -0
- package/build/templates/overlays/researcher-library.mdx +59 -0
- package/build/templates/overlays/researcher-performance.mdx +68 -0
- package/build/templates/overlays/researcher-security.mdx +86 -0
- package/build/templates/overlays/tester-graphql.mdx +191 -0
- package/build/templates/overlays/tester-playwright.mdx +621 -0
- package/build/templates/overlays/tester-rest.mdx +101 -0
- package/build/templates/overlays/tester-suite.mdx +177 -0
- package/build/templates/super-coder.mdx +529 -0
- package/build/templates/super-planner.mdx +568 -0
- package/build/templates/super-researcher.mdx +406 -0
- package/build/templates/super-tester.mdx +243 -0
- package/build/tools/answer-question.d.ts +30 -0
- package/build/tools/answer-question.d.ts.map +1 -0
- package/build/tools/answer-question.js +108 -0
- package/build/tools/answer-question.js.map +1 -0
- package/build/tools/cancel-task.d.ts +44 -0
- package/build/tools/cancel-task.d.ts.map +1 -0
- package/build/tools/cancel-task.js +144 -0
- package/build/tools/cancel-task.js.map +1 -0
- package/build/tools/send-message.d.ts +39 -0
- package/build/tools/send-message.d.ts.map +1 -0
- package/build/tools/send-message.js +124 -0
- package/build/tools/send-message.js.map +1 -0
- package/build/tools/shared-spawn.d.ts +56 -0
- package/build/tools/shared-spawn.d.ts.map +1 -0
- package/build/tools/shared-spawn.js +114 -0
- package/build/tools/shared-spawn.js.map +1 -0
- package/build/tools/spawn-agent.d.ts +85 -0
- package/build/tools/spawn-agent.d.ts.map +1 -0
- package/build/tools/spawn-agent.js +133 -0
- package/build/tools/spawn-agent.js.map +1 -0
- package/build/tools/spawn-coder.d.ts +70 -0
- package/build/tools/spawn-coder.d.ts.map +1 -0
- package/build/tools/spawn-coder.js +71 -0
- package/build/tools/spawn-coder.js.map +1 -0
- package/build/tools/spawn-planner.d.ts +70 -0
- package/build/tools/spawn-planner.d.ts.map +1 -0
- package/build/tools/spawn-planner.js +71 -0
- package/build/tools/spawn-planner.js.map +1 -0
- package/build/tools/spawn-researcher.d.ts +70 -0
- package/build/tools/spawn-researcher.d.ts.map +1 -0
- package/build/tools/spawn-researcher.js +70 -0
- package/build/tools/spawn-researcher.js.map +1 -0
- package/build/tools/spawn-task.d.ts +74 -0
- package/build/tools/spawn-task.d.ts.map +1 -0
- package/build/tools/spawn-task.js +107 -0
- package/build/tools/spawn-task.js.map +1 -0
- package/build/tools/spawn-tester.d.ts +70 -0
- package/build/tools/spawn-tester.d.ts.map +1 -0
- package/build/tools/spawn-tester.js +69 -0
- package/build/tools/spawn-tester.js.map +1 -0
- package/build/types.d.ts +101 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +28 -0
- package/build/types.js.map +1 -0
- package/build/utils/brief-validator.d.ts +30 -0
- package/build/utils/brief-validator.d.ts.map +1 -0
- package/build/utils/brief-validator.js +254 -0
- package/build/utils/brief-validator.js.map +1 -0
- package/build/utils/format.d.ts +34 -0
- package/build/utils/format.d.ts.map +1 -0
- package/build/utils/format.js +55 -0
- package/build/utils/format.js.map +1 -0
- package/build/utils/sanitize.d.ts +240 -0
- package/build/utils/sanitize.d.ts.map +1 -0
- package/build/utils/sanitize.js +89 -0
- package/build/utils/sanitize.js.map +1 -0
- package/build/utils/task-id-generator.d.ts +10 -0
- package/build/utils/task-id-generator.d.ts.map +1 -0
- package/build/utils/task-id-generator.js +22 -0
- package/build/utils/task-id-generator.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
## SUPABASE-SPECIFIC GUIDELINES
|
|
2
|
+
|
|
3
|
+
You are working in a codebase that uses **Supabase**. Apply these principles with zero exceptions.
|
|
4
|
+
|
|
5
|
+
### Client Initialization — Choose the Right Client
|
|
6
|
+
|
|
7
|
+
- **Browser / Client Components:** Use `createBrowserClient` from `@supabase/ssr` — NOT `createClient` from `@supabase/supabase-js`
|
|
8
|
+
- **Server Components / Server Actions / Route Handlers:** Use `createServerClient` from `@supabase/ssr` with cookie methods (`getAll`, `setAll`)
|
|
9
|
+
- **Edge Functions / server-only scripts:** Use `createClient` from `@supabase/supabase-js` — this is the ONLY place it belongs in an SSR app
|
|
10
|
+
- **Service role client:** Create with `createServerClient` using the service role key and empty cookie methods (`getAll: () => []`). NEVER use in client code
|
|
11
|
+
- **NEVER create a singleton server client** — server clients are request-scoped (cookies are per-request). Use a factory function like `createServerSupabaseClient()` called per-request
|
|
12
|
+
- **ALWAYS pass `<Database>` generic** to every client: `createClient<Database>(url, key)`
|
|
13
|
+
- Project structure: `lib/supabase/client.ts` (browser), `lib/supabase/server.ts` (server), `lib/supabase/middleware.ts` (session refresh)
|
|
14
|
+
|
|
15
|
+
### Middleware — NON-NEGOTIABLE for SSR Auth
|
|
16
|
+
|
|
17
|
+
- **ALWAYS implement middleware** that calls `updateSession()` on every request — without it, sessions silently expire, causing random logouts
|
|
18
|
+
- Use `matcher` config to exclude static assets: `matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)']`
|
|
19
|
+
- The middleware refreshes the JWT and sets updated cookies — if you skip this, auth WILL break
|
|
20
|
+
- Wrap `setAll` in try/catch in Server Components (cookies are read-only there)
|
|
21
|
+
|
|
22
|
+
### TypeScript Types — Generate and Use Them
|
|
23
|
+
|
|
24
|
+
- **ALWAYS generate types:** `npx supabase gen types typescript --project-id "$PROJECT_ID" > src/database.types.ts`
|
|
25
|
+
- For local dev: `npx supabase gen types typescript --local > src/database.types.ts`
|
|
26
|
+
- Add a `package.json` script: `"gen:types": "supabase gen types typescript --project-id $PROJECT_ID > src/database.types.ts"`
|
|
27
|
+
- **Extract row types properly:** `type User = Database['public']['Tables']['users']['Row']`
|
|
28
|
+
- **Insert types:** `type NewUser = Database['public']['Tables']['users']['Insert']`
|
|
29
|
+
- **Update types:** `type UserUpdate = Database['public']['Tables']['users']['Update']`
|
|
30
|
+
- Regenerate types after EVERY schema change — never hand-edit `database.types.ts`
|
|
31
|
+
- If a view column appears incorrectly as nullable, use `MergeDeep` from `type-fest` to override
|
|
32
|
+
- Known issue: field ordering in generated types can be inconsistent across runs — do NOT rely on git diff of types for CI gates
|
|
33
|
+
|
|
34
|
+
### Row Level Security (RLS) — ALWAYS ENABLE
|
|
35
|
+
|
|
36
|
+
- **Enable RLS on EVERY table** in the `public` schema — no exceptions. Tables without RLS are fully exposed via the API
|
|
37
|
+
- RLS with no policies = zero access (not even authenticated users). Always create policies after enabling
|
|
38
|
+
- **Wrap auth functions in `(select ...)`** for performance: use `(select auth.uid()) = user_id` NOT `auth.uid() = user_id` — this caches the result per-statement instead of evaluating per-row (100x+ improvement on large tables)
|
|
39
|
+
- **Target specific roles:** Add `TO authenticated` on policies — don't rely on `auth.uid()` alone to exclude `anon`
|
|
40
|
+
- **Separate policies per operation:** Create distinct SELECT, INSERT, UPDATE, DELETE policies — never one catch-all
|
|
41
|
+
- **NEVER use `user_metadata` in RLS policies** — users can modify their own `user_metadata` via the API
|
|
42
|
+
- **Add indexes on RLS-referenced columns** — e.g., if policy checks `user_id`, add `CREATE INDEX idx_table_user_id ON table(user_id)`
|
|
43
|
+
- For team/tenant access patterns: `team_id IN (SELECT team_id FROM team_members WHERE user_id = (select auth.uid()))` — always put the user filter in the subquery, not the outer query
|
|
44
|
+
- **Use security definer functions** for complex RLS logic — but put them in a non-public schema to prevent API exposure
|
|
45
|
+
- Views bypass RLS by default. For Postgres 15+, use `CREATE VIEW ... WITH (security_invoker = true)`
|
|
46
|
+
|
|
47
|
+
### Database Queries — Correct Patterns
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// CORRECT: Select with typed response
|
|
51
|
+
const { data, error } = await supabase
|
|
52
|
+
.from('posts')
|
|
53
|
+
.select('id, title, author:profiles(name, avatar_url)')
|
|
54
|
+
.eq('published', true)
|
|
55
|
+
.order('created_at', { ascending: false })
|
|
56
|
+
.limit(20)
|
|
57
|
+
|
|
58
|
+
// CORRECT: Insert with returning data
|
|
59
|
+
const { data, error } = await supabase
|
|
60
|
+
.from('posts')
|
|
61
|
+
.insert({ title, body, user_id: userId })
|
|
62
|
+
.select()
|
|
63
|
+
.single()
|
|
64
|
+
|
|
65
|
+
// CORRECT: Update with filter
|
|
66
|
+
const { data, error } = await supabase
|
|
67
|
+
.from('posts')
|
|
68
|
+
.update({ title: newTitle })
|
|
69
|
+
.eq('id', postId)
|
|
70
|
+
.select()
|
|
71
|
+
.single()
|
|
72
|
+
|
|
73
|
+
// CORRECT: Null check — use .is(), NOT .eq()
|
|
74
|
+
const { data } = await supabase
|
|
75
|
+
.from('posts')
|
|
76
|
+
.select()
|
|
77
|
+
.is('deleted_at', null)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **ALWAYS handle `error`** — never destructure only `data`. Check `if (error) throw error` or handle gracefully
|
|
81
|
+
- **Select only needed columns** — `.select('id, name')` not `.select('*')` — reduces payload and improves performance
|
|
82
|
+
- Use `.single()` when expecting exactly one row, `.maybeSingle()` when zero or one
|
|
83
|
+
- For joins: `.select('*, comments(*)')` uses PostgREST resource embedding — requires foreign key relationships
|
|
84
|
+
- For `.rpc()` joins to work, Postgres functions MUST use `RETURNS SETOF <table_name>` — NOT `RETURNS TABLE(...)`
|
|
85
|
+
- Use `.or()` for OR conditions: `.or('status.eq.active,status.eq.pending')`
|
|
86
|
+
- Paginate with `.range(from, to)` — never fetch unbounded results
|
|
87
|
+
|
|
88
|
+
### Auth — Patterns and Anti-Patterns
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// CORRECT: Server-side user check (ALWAYS use getUser, not getSession for auth checks)
|
|
92
|
+
const { data: { user }, error } = await supabase.auth.getUser()
|
|
93
|
+
if (!user) redirect('/login')
|
|
94
|
+
|
|
95
|
+
// CORRECT: Sign up
|
|
96
|
+
const { data, error } = await supabase.auth.signUp({
|
|
97
|
+
email,
|
|
98
|
+
password,
|
|
99
|
+
options: { data: { full_name: name } } // goes to user_metadata
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// CORRECT: Sign in with password
|
|
103
|
+
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
|
|
104
|
+
|
|
105
|
+
// CORRECT: Magic link / OTP
|
|
106
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
107
|
+
email,
|
|
108
|
+
options: { shouldCreateUser: false } // prevent auto-signup
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// CORRECT: OAuth
|
|
112
|
+
const { data, error } = await supabase.auth.signInWithOAuth({
|
|
113
|
+
provider: 'google',
|
|
114
|
+
options: { redirectTo: `${origin}/auth/callback` }
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// CORRECT: Auth callback route handler (app/auth/callback/route.ts)
|
|
118
|
+
const { searchParams } = new URL(request.url)
|
|
119
|
+
const code = searchParams.get('code')
|
|
120
|
+
if (code) {
|
|
121
|
+
const supabase = await createServerSupabaseClient()
|
|
122
|
+
await supabase.auth.exchangeCodeForSession(code)
|
|
123
|
+
}
|
|
124
|
+
return NextResponse.redirect(new URL('/dashboard', request.url))
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- **Use `getUser()` for auth verification, not `getSession()`** — `getSession` reads from cookies without server validation; `getUser` makes a server call to verify the JWT
|
|
128
|
+
- **PKCE flow is the default** in `@supabase/ssr` — don't switch to implicit flow
|
|
129
|
+
- **ALWAYS implement the `/auth/callback` route** for PKCE code exchange
|
|
130
|
+
- Never store tokens manually — `@supabase/ssr` handles cookie storage automatically
|
|
131
|
+
- For magic links: edit the email template to use `{{ .TokenHash }}` with `type=magiclink` params for PKCE compatibility
|
|
132
|
+
- Session data from the server can be passed to client via React Context (`AuthProvider`) to avoid redundant `getUser()` calls
|
|
133
|
+
|
|
134
|
+
### Storage — Buckets and Uploads
|
|
135
|
+
|
|
136
|
+
- **Default to private buckets** — public buckets bypass download access control entirely
|
|
137
|
+
- Private bucket access requires either: a valid JWT in the Authorization header, or a signed URL
|
|
138
|
+
- **RLS policies on `storage.objects`** control all storage operations — no policies = no uploads allowed
|
|
139
|
+
- **Use signed upload URLs** for client-side uploads to avoid body size limits (Next.js defaults to 1MB):
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Server Action: create signed upload URL
|
|
143
|
+
const { data, error } = await supabase.storage
|
|
144
|
+
.from('avatars')
|
|
145
|
+
.createSignedUploadUrl(`${userId}/${fileName}`)
|
|
146
|
+
|
|
147
|
+
// Client: upload to signed URL
|
|
148
|
+
const { error } = await supabase.storage
|
|
149
|
+
.from('avatars')
|
|
150
|
+
.uploadToSignedUrl(data.path, data.token, file)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **Scope file paths to user IDs:** `{user_id}/filename.ext` — then write RLS policies that match `(storage.foldername(name))[1] = (select auth.uid())::text`
|
|
154
|
+
- Signed download URLs have a configurable expiry — keep it as short as practical
|
|
155
|
+
- For public assets (logos, static images): use a public bucket but understand anyone with the URL can access the file
|
|
156
|
+
- **Upsert requires SELECT + UPDATE + INSERT policies** — not just INSERT
|
|
157
|
+
|
|
158
|
+
### Edge Functions — Deno Runtime
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
// CORRECT: Modern Edge Function pattern
|
|
162
|
+
Deno.serve(async (req: Request) => {
|
|
163
|
+
// CORS handling
|
|
164
|
+
if (req.method === 'OPTIONS') {
|
|
165
|
+
return new Response('ok', {
|
|
166
|
+
headers: {
|
|
167
|
+
'Access-Control-Allow-Origin': '*',
|
|
168
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
169
|
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create Supabase client with user's JWT
|
|
175
|
+
const supabase = createClient<Database>(
|
|
176
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
177
|
+
Deno.env.get('SUPABASE_ANON_KEY')!,
|
|
178
|
+
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } }
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
182
|
+
// ... function logic
|
|
183
|
+
|
|
184
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
185
|
+
headers: { 'Content-Type': 'application/json' },
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **Use `Deno.serve()`** — do NOT import `serve` from `deno.land/std`
|
|
191
|
+
- **Import specifiers:** Use `npm:@supabase/supabase-js` — NOT bare `@supabase/supabase-js`. Always pin versions: `npm:express@4.18.2`
|
|
192
|
+
- **Pre-populated env vars:** `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`, `SUPABASE_DB_URL` are available automatically — do NOT set them manually
|
|
193
|
+
- **Shared code** goes in `supabase/functions/_shared/` — import via relative path. Never cross-import between functions
|
|
194
|
+
- **Fat functions pattern:** Group related routes into one function using Hono or Express — minimizes cold starts
|
|
195
|
+
- Use `EdgeRuntime.waitUntil(promise)` for background tasks that shouldn't block the response
|
|
196
|
+
- File writes only allowed in `/tmp`
|
|
197
|
+
- Design for idempotency — Edge Functions can be retried
|
|
198
|
+
|
|
199
|
+
### Realtime — Use Wisely
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// Postgres Changes (simple, but doesn't scale well)
|
|
203
|
+
const channel = supabase
|
|
204
|
+
.channel('posts-changes')
|
|
205
|
+
.on('postgres_changes',
|
|
206
|
+
{ event: 'INSERT', schema: 'public', table: 'posts', filter: 'room_id=eq.123' },
|
|
207
|
+
(payload) => handleNewPost(payload.new)
|
|
208
|
+
)
|
|
209
|
+
.subscribe()
|
|
210
|
+
|
|
211
|
+
// Broadcast (preferred for most real-time use cases)
|
|
212
|
+
const channel = supabase.channel('room-123')
|
|
213
|
+
channel.on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
|
|
214
|
+
updateCursor(payload)
|
|
215
|
+
})
|
|
216
|
+
channel.subscribe()
|
|
217
|
+
channel.send({ type: 'broadcast', event: 'cursor-move', payload: { x, y } })
|
|
218
|
+
|
|
219
|
+
// Presence
|
|
220
|
+
const channel = supabase.channel('room-123')
|
|
221
|
+
channel.on('presence', { event: 'sync' }, () => {
|
|
222
|
+
const state = channel.presenceState()
|
|
223
|
+
updateOnlineUsers(state)
|
|
224
|
+
})
|
|
225
|
+
channel.subscribe(async (status) => {
|
|
226
|
+
if (status === 'SUBSCRIBED') {
|
|
227
|
+
await channel.track({ user_id: userId, online_at: new Date().toISOString() })
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- **Prefer Broadcast over Postgres Changes** for scale — Postgres Changes checks RLS for every subscriber on every change (100 subscribers = 100 reads per change)
|
|
233
|
+
- **Unsubscribe on cleanup:** `supabase.removeChannel(channel)` in `useEffect` cleanup / `onUnmounted`
|
|
234
|
+
- Postgres Changes are processed on a single thread — they do NOT scale with compute upgrades
|
|
235
|
+
- Realtime does NOT guarantee delivery — design your app to handle missed messages
|
|
236
|
+
- Default limits: 100 channels per tenant, 200 concurrent users per channel, 100 events/second
|
|
237
|
+
- For high-throughput: use a dedicated "broadcast" table without RLS and filter on the client
|
|
238
|
+
|
|
239
|
+
### API Keys — Critical Security Rules
|
|
240
|
+
|
|
241
|
+
- **Anon key / publishable key** (`NEXT_PUBLIC_SUPABASE_ANON_KEY`): Safe for client-side. RLS protects your data. This key is meant to be public
|
|
242
|
+
- **Service role key / secret key**: Bypasses ALL RLS. **NEVER expose in client code, NEVER prefix with `NEXT_PUBLIC_`**
|
|
243
|
+
- The service role key goes in server-only environment variables
|
|
244
|
+
- When creating a client with the service role key, the `Authorization` header defaults to that key — so ALL queries bypass RLS automatically
|
|
245
|
+
- **Key rotation:** Supabase is migrating from JWT-based anon/service_role keys to publishable (`sb_publishable_...`) and secret keys. Be aware that Edge Functions may need `--no-verify-jwt` with the new key format
|
|
246
|
+
- In Edge Functions: verify the user via `supabase.auth.getUser()` — do NOT trust the JWT blindly
|
|
247
|
+
|
|
248
|
+
### Connection Management
|
|
249
|
+
|
|
250
|
+
- **Use Supavisor (connection pooler)** for server-side connections from ORMs (Prisma, Drizzle) — port `6543` for Transaction Mode, port `5432` for Session Mode
|
|
251
|
+
- **Transaction Mode** (port 6543): Shares connections, higher throughput, but NO prepared statements — incompatible with Prisma unless using `@prisma/adapter-pg`
|
|
252
|
+
- **Session Mode** (port 5432): Supports prepared statements, required by Prisma's default adapter
|
|
253
|
+
- If using the Supabase client library (`supabase-js`), you do NOT need Supavisor — it uses the REST API (PostgREST), not direct Postgres connections
|
|
254
|
+
- Keep pooler usage under 40% of max connections if also using REST API, Auth, Realtime, and Storage (they share the connection pool)
|
|
255
|
+
- Add `?pgbouncer=true` to connection strings when using Prisma with Supavisor
|
|
256
|
+
|
|
257
|
+
### Common Anti-Patterns — NEVER Do These
|
|
258
|
+
|
|
259
|
+
- **NEVER disable RLS** "to make things work" — fix your policies instead
|
|
260
|
+
- **NEVER use `select('*')` in production** — select only the columns you need
|
|
261
|
+
- **NEVER put business logic in RLS policies** — keep RLS simple (auth checks only), put business logic in application code or Edge Functions
|
|
262
|
+
- **NEVER manage schema through the Dashboard UI in production** — use migrations (`supabase migration new`, SQL files in `supabase/migrations/`)
|
|
263
|
+
- **NEVER call `getSession()` for auth verification** on the server — it reads cookies without validating the JWT. Use `getUser()` instead
|
|
264
|
+
- **NEVER store the service role key in `NEXT_PUBLIC_*` env vars** or any client-accessible location
|
|
265
|
+
- **NEVER use `user_metadata` for authorization decisions** — users can modify their own metadata
|
|
266
|
+
- **NEVER create tables without thinking about RLS first** — add policies immediately after table creation
|
|
267
|
+
- **NEVER use bare `catch` without handling** — Supabase errors contain `code`, `message`, `details`, and `hint` fields. Log them all
|
|
268
|
+
- **NEVER dump everything into the `public` schema** — use custom schemas for internal tables, keeping only API-facing tables in `public`
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
## SUPASTARTER CODING GUIDELINES
|
|
2
|
+
|
|
3
|
+
You are working in a **Supastarter** monorepo (Next.js App Router). Follow these conventions exactly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
### MONOREPO STRUCTURE
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
apps/
|
|
11
|
+
web/ # Next.js SaaS app (App Router)
|
|
12
|
+
mail-preview/ # Email template previewer
|
|
13
|
+
docs/ # Documentation site
|
|
14
|
+
packages/
|
|
15
|
+
api/ # oRPC procedures & Hono handlers
|
|
16
|
+
auth/ # Better Auth config
|
|
17
|
+
database/ # Prisma + Drizzle schema & queries
|
|
18
|
+
ui/ # Shadcn UI components
|
|
19
|
+
mail/ # React Email templates
|
|
20
|
+
payments/ # Stripe/Polar/other providers
|
|
21
|
+
ai/ # Vercel AI SDK wrappers
|
|
22
|
+
i18n/ # next-intl config & translations
|
|
23
|
+
logs/ # Logger utilities
|
|
24
|
+
storage/ # S3/file storage
|
|
25
|
+
utils/ # Shared utilities
|
|
26
|
+
config/ # Workspace-level config
|
|
27
|
+
tooling/ # Build tooling & shared configs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### IMPORTS & PATH ALIASES
|
|
33
|
+
|
|
34
|
+
Always use path aliases. Never use deep relative imports.
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Good
|
|
38
|
+
import { Button } from "@repo/ui/components/button";
|
|
39
|
+
import { cn } from "@repo/ui";
|
|
40
|
+
import { getSession } from "@saas/auth/lib/server";
|
|
41
|
+
import { db } from "@repo/database";
|
|
42
|
+
|
|
43
|
+
// Bad
|
|
44
|
+
import { Button } from "../../../packages/ui/components/button.tsx";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Alias | Maps To |
|
|
48
|
+
|-------|---------|
|
|
49
|
+
| `@/*` | `apps/web/*` |
|
|
50
|
+
| `@marketing/*` | `apps/web/modules/marketing/*` |
|
|
51
|
+
| `@saas/*` | `apps/web/modules/saas/*` |
|
|
52
|
+
| `@shared/*` | `apps/web/modules/shared/*` |
|
|
53
|
+
| `@repo/*` | `packages/*` |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### REACT SERVER COMPONENTS
|
|
58
|
+
|
|
59
|
+
Default to Server Components. Only add `"use client"` for interactivity, browser APIs, hooks (`useQuery`, `useMutation`), or forms.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// Server Component (default)
|
|
63
|
+
import { getSession } from "@saas/auth/lib/server";
|
|
64
|
+
import { redirect } from "next/navigation";
|
|
65
|
+
|
|
66
|
+
export default async function ProtectedPage() {
|
|
67
|
+
const session = await getSession();
|
|
68
|
+
if (!session) redirect("/auth/login");
|
|
69
|
+
return <div>Welcome, {session.user.name}</div>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Client Component (only when necessary)
|
|
73
|
+
"use client";
|
|
74
|
+
import { useState } from "react";
|
|
75
|
+
export function Counter() {
|
|
76
|
+
const [count, setCount] = useState(0);
|
|
77
|
+
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Keep client components small and focused.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### MODULE ORGANIZATION
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
apps/web/modules/
|
|
89
|
+
saas/
|
|
90
|
+
auth/ # components/, hooks/, lib/, constants/, config.ts
|
|
91
|
+
organizations/ # components/, hooks/, lib/
|
|
92
|
+
payments/
|
|
93
|
+
settings/
|
|
94
|
+
admin/
|
|
95
|
+
ai/
|
|
96
|
+
shared/ # Cross-cutting components, hooks, lib
|
|
97
|
+
marketing/ # Marketing-only components, lib
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### API LAYER (oRPC)
|
|
103
|
+
|
|
104
|
+
Procedures live at `packages/api/modules/[feature]/procedures/[action].ts`:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { protectedProcedure } from "../../../orpc/procedures";
|
|
108
|
+
import { z } from "zod";
|
|
109
|
+
import { ORPCError } from "@orpc/client";
|
|
110
|
+
|
|
111
|
+
export const submitContactForm = publicProcedure
|
|
112
|
+
.route({ method: "POST", path: "/contact", tags: ["Contact"] })
|
|
113
|
+
.input(z.object({
|
|
114
|
+
name: z.string().min(3),
|
|
115
|
+
email: z.string().email(),
|
|
116
|
+
message: z.string().min(10),
|
|
117
|
+
}))
|
|
118
|
+
.use(localeMiddleware)
|
|
119
|
+
.handler(async ({ input, context }) => {
|
|
120
|
+
// Implementation
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Procedure types:** `publicProcedure` (no auth), `protectedProcedure` (requires session), `adminProcedure` (requires admin role).
|
|
125
|
+
|
|
126
|
+
**Client-side data fetching:**
|
|
127
|
+
```typescript
|
|
128
|
+
"use client";
|
|
129
|
+
import { useQuery } from "@tanstack/react-query";
|
|
130
|
+
import { orpc } from "@shared/lib/orpc-query-utils";
|
|
131
|
+
|
|
132
|
+
export function UsersList() {
|
|
133
|
+
const { data, isLoading } = useQuery(orpc.users.list.queryOptions());
|
|
134
|
+
if (isLoading) return <Skeleton />;
|
|
135
|
+
return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### DATABASE (Drizzle)
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { eq } from "drizzle-orm";
|
|
145
|
+
import { db } from "../client";
|
|
146
|
+
|
|
147
|
+
export async function getUserById(id: string) {
|
|
148
|
+
return await db.query.user.findFirst({
|
|
149
|
+
where: (user, { eq }) => eq(user.id, id),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function createUser(data: UserCreateInput) {
|
|
154
|
+
const [{ id }] = await db
|
|
155
|
+
.insert(user)
|
|
156
|
+
.values({ ...data, createdAt: new Date(), updatedAt: new Date() })
|
|
157
|
+
.returning({ id: user.id });
|
|
158
|
+
return await getUserById(id);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Import from `@repo/database` exports — never raw Prisma imports.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
### AUTH (Better Auth)
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Server-side (ALWAYS use getSession from @saas/auth/lib/server)
|
|
170
|
+
const session = await getSession();
|
|
171
|
+
if (!session) redirect("/auth/login");
|
|
172
|
+
|
|
173
|
+
// Client-side
|
|
174
|
+
"use client";
|
|
175
|
+
import { useSession } from "@saas/auth/hooks/use-session";
|
|
176
|
+
const { user, loaded } = useSession();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Features: email/password, OAuth (Google, GitHub), magic link, passkey, 2FA, organization-scoped.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### UI COMPONENTS (Shadcn + CVA)
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { cva } from "class-variance-authority";
|
|
187
|
+
import { cn } from "../lib";
|
|
188
|
+
|
|
189
|
+
const buttonVariants = cva(
|
|
190
|
+
"flex items-center justify-center font-medium transition-colors",
|
|
191
|
+
{
|
|
192
|
+
variants: {
|
|
193
|
+
variant: { primary: "bg-primary text-primary-foreground", secondary: "bg-secondary" },
|
|
194
|
+
size: { sm: "h-6 px-3 text-xs", md: "h-9 px-4 text-sm", lg: "h-12 px-6 text-base" },
|
|
195
|
+
},
|
|
196
|
+
defaultVariants: { variant: "secondary", size: "md" },
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Use `cn()` for class merging. Use `<Button loading={...}>` prop for loading states.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### FORMS (React Hook Form + Zod)
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
"use client";
|
|
209
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
210
|
+
import { useForm } from "react-hook-form";
|
|
211
|
+
import { z } from "zod";
|
|
212
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@repo/ui/components/form";
|
|
213
|
+
|
|
214
|
+
const formSchema = z.object({ email: z.string().email(), name: z.string().min(3) });
|
|
215
|
+
|
|
216
|
+
export function ContactForm() {
|
|
217
|
+
const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { email: "", name: "" } });
|
|
218
|
+
return (
|
|
219
|
+
<Form {...form}>
|
|
220
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
221
|
+
<FormField control={form.control} name="email"
|
|
222
|
+
render={({ field }) => (
|
|
223
|
+
<FormItem><FormLabel>Email</FormLabel><FormControl><Input {...field} /></FormControl><FormMessage /></FormItem>
|
|
224
|
+
)} />
|
|
225
|
+
<Button type="submit" loading={form.formState.isSubmitting}>Submit</Button>
|
|
226
|
+
</form>
|
|
227
|
+
</Form>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### I18N (next-intl)
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Server Component
|
|
238
|
+
import { getTranslations, setRequestLocale } from "next-intl/server";
|
|
239
|
+
const t = await getTranslations();
|
|
240
|
+
return <h1>{t("home.welcome.title")}</h1>;
|
|
241
|
+
|
|
242
|
+
// Client Component
|
|
243
|
+
"use client";
|
|
244
|
+
import { useTranslations } from "next-intl";
|
|
245
|
+
const t = useTranslations();
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### NAMING CONVENTIONS
|
|
251
|
+
|
|
252
|
+
| Type | Convention | Example |
|
|
253
|
+
|------|-----------|---------|
|
|
254
|
+
| Directories | kebab-case | `components/auth-form/` |
|
|
255
|
+
| Components | PascalCase.tsx | `LoginForm.tsx` |
|
|
256
|
+
| Functions | camelCase | `getUser()`, `handleSubmit()` |
|
|
257
|
+
| Booleans | is/has/can prefix | `isLoading`, `hasError` |
|
|
258
|
+
| Constants | SCREAMING_SNAKE | `MAX_RETRIES` |
|
|
259
|
+
| Types | PascalCase | `UserProps`, `AuthConfig` |
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### TYPESCRIPT RULES
|
|
264
|
+
|
|
265
|
+
- Named exports only — never default exports
|
|
266
|
+
- `as const` assertions — never enums
|
|
267
|
+
- Strict typing — no `any` without justification
|
|
268
|
+
- Client env vars: prefix with `NEXT_PUBLIC_`
|
|
269
|
+
- Use `@repo/logs` logger — never `console.log` in production
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### CONFIGURATION
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// apps/web/config.ts
|
|
277
|
+
export const config = {
|
|
278
|
+
appName: "supastarter",
|
|
279
|
+
saas: { enabled: true, redirectAfterSignIn: "/app" },
|
|
280
|
+
marketing: { enabled: true },
|
|
281
|
+
} as const;
|
|
282
|
+
|
|
283
|
+
// Import: import { config } from "@/config";
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Each package has scoped config. Import via `@repo/[package]/config`.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
### BUILD & TOOLING
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
pnpm dev # Start dev (Turbo)
|
|
294
|
+
pnpm build # Build all packages
|
|
295
|
+
pnpm type-check # TypeScript check
|
|
296
|
+
pnpm lint # Biome lint
|
|
297
|
+
pnpm format # Biome format
|
|
298
|
+
pnpm e2e # Playwright E2E tests
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### ANTI-PATTERNS
|
|
304
|
+
|
|
305
|
+
- Deep relative imports — use path aliases
|
|
306
|
+
- Default exports — use named exports
|
|
307
|
+
- Enums — use `as const` + type extraction
|
|
308
|
+
- `select('*')` — select only needed columns
|
|
309
|
+
- Raw Prisma imports — use `@repo/database`
|
|
310
|
+
- `console.log` — use `@repo/logs` logger
|
|
311
|
+
- Hardcoded strings — use i18n translations
|
|
312
|
+
- Missing `NEXT_PUBLIC_` prefix — server vars won't reach the client
|
|
313
|
+
- Singleton server Supabase clients — server clients are request-scoped
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
## SWIFT-SPECIFIC GUIDELINES
|
|
2
|
+
|
|
3
|
+
You are working in a **Swift** codebase. Apply these principles with zero exceptions.
|
|
4
|
+
|
|
5
|
+
### Protocol-Oriented Programming
|
|
6
|
+
- Prefer **protocols** over class inheritance — Swift is protocol-oriented, not class-oriented
|
|
7
|
+
- Use protocol extensions for default implementations — avoid abstract base classes
|
|
8
|
+
- Use `associatedtype` for generic protocol requirements
|
|
9
|
+
- Compose behavior with multiple protocol conformances: `struct User: Identifiable, Codable, Hashable`
|
|
10
|
+
- Use `some Protocol` (opaque return types) to hide implementation details
|
|
11
|
+
|
|
12
|
+
### Optionals — Safety First
|
|
13
|
+
- Use `guard let` for early exit unwrapping — keep the happy path unindented
|
|
14
|
+
- Use `if let` for conditional unwrapping when the `else` case continues execution
|
|
15
|
+
- **Never force-unwrap** (`!`) in production code — use `guard let`, `if let`, or `??`
|
|
16
|
+
- Use nil-coalescing `??` for default values: `let name = user?.name ?? "Unknown"`
|
|
17
|
+
- Use optional chaining `?.` for accessing nested optionals
|
|
18
|
+
- Use `compactMap` to filter out nils from collections
|
|
19
|
+
|
|
20
|
+
### Value vs Reference Types
|
|
21
|
+
- Prefer **structs** (value types) over classes — structs are the default in Swift
|
|
22
|
+
- Use **classes** only when you need: identity (reference semantics), inheritance, deinit, or Objective-C interop
|
|
23
|
+
- Use `mutating` keyword on struct methods that modify `self`
|
|
24
|
+
- Understand copy-on-write behavior for large value types (Array, String, Dictionary have COW built in)
|
|
25
|
+
|
|
26
|
+
### Error Handling
|
|
27
|
+
- Use `throws` functions with typed errors when possible (Swift 6+)
|
|
28
|
+
- Create error enums conforming to `Error`: `enum AuthError: Error { case invalidToken, expired }`
|
|
29
|
+
- Use `do/try/catch` with specific error cases — avoid `try?` unless you intentionally want to discard the error
|
|
30
|
+
- Use `Result<Success, Failure>` for asynchronous error handling in callback-based APIs
|
|
31
|
+
|
|
32
|
+
### Modern Swift Patterns
|
|
33
|
+
- Use **async/await** for concurrency (Swift 5.5+) — prefer over completion handlers
|
|
34
|
+
- Use **actors** for thread-safe mutable state
|
|
35
|
+
- Use `@Sendable` closures and `Sendable` types for concurrency safety
|
|
36
|
+
- Use `Task` and `TaskGroup` for structured concurrency
|
|
37
|
+
- Use `@MainActor` for UI-related code
|
|
38
|
+
|
|
39
|
+
### SwiftUI Patterns (if applicable)
|
|
40
|
+
- Use `@State` for view-local state, `@Binding` for parent-owned state
|
|
41
|
+
- Use `@ObservedObject` / `@StateObject` for reference-type view models
|
|
42
|
+
- Use `@Environment` for dependency injection from the environment
|
|
43
|
+
- Keep views small — extract subviews as separate structs
|
|
44
|
+
- Use `ViewModifier` for reusable view modifications
|
|
45
|
+
|
|
46
|
+
### Naming Conventions
|
|
47
|
+
- `camelCase` for functions, methods, properties, variables
|
|
48
|
+
- `PascalCase` for types, protocols, enums
|
|
49
|
+
- Follow Swift API Design Guidelines: methods read as English phrases
|
|
50
|
+
- Factory methods: `make*` (e.g., `makeIterator()`)
|
|
51
|
+
- Boolean properties: `is*`, `has*`, `can*`, `should*`
|
|
52
|
+
|
|
53
|
+
### Testing Considerations
|
|
54
|
+
- Use protocol-based dependency injection for testability
|
|
55
|
+
- Prefer value types that are `Equatable` — easy to assert in tests
|
|
56
|
+
- Use `XCTAssert*` or project's testing framework conventions
|