@riligar/agents-kit 1.15.0 → 1.17.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.
Files changed (26) hide show
  1. package/.agent/skills/riligar-design-system/SKILL.md +1 -0
  2. package/.agent/skills/riligar-design-system/references/design-system.md +0 -7
  3. package/.agent/skills/riligar-dev-auth-elysia/SKILL.md +2 -2
  4. package/.agent/skills/riligar-dev-dashboard/SKILL.md +331 -1
  5. package/.agent/skills/riligar-dev-dashboard/references/dependencies.md +44 -0
  6. package/.agent/skills/riligar-dev-database/SKILL.md +45 -0
  7. package/.agent/skills/riligar-dev-database/references/connection.md +74 -0
  8. package/.agent/skills/riligar-dev-database/references/migrations.md +70 -0
  9. package/.agent/skills/riligar-dev-database/references/queries.md +173 -0
  10. package/.agent/skills/riligar-dev-database/references/schema.md +106 -0
  11. package/.agent/skills/riligar-dev-manager/SKILL.md +13 -8
  12. package/.agent/skills/riligar-dev-website/SKILL.md +1 -1
  13. package/.agent/skills/riligar-dev-website-seo/SKILL.md +1 -1
  14. package/.agent/skills/riligar-infra-cloudfare/SKILL.md +350 -0
  15. package/.agent/skills/riligar-infra-cloudfare/references/cloudflare-api.md +139 -0
  16. package/.agent/skills/riligar-infra-cloudfare/references/email-routing.md +262 -0
  17. package/.agent/skills/riligar-infra-cloudfare/references/r2-storage.md +333 -0
  18. package/.agent/skills/riligar-infra-fly/SKILL.md +38 -1
  19. package/.agent/skills/riligar-infra-stripe/SKILL.md +2 -3
  20. package/.agent/skills/riligar-marketing-email/SKILL.md +0 -6
  21. package/.agent/skills/riligar-marketing-seo/SKILL.md +1 -7
  22. package/package.json +1 -1
  23. package/.agent/skills/riligar-dev-stack/SKILL.md +0 -110
  24. package/.agent/skills/riligar-dev-stack/references/tech-stack.md +0 -131
  25. package/.agent/skills/riligar-infra-cloudfare/.gitkeep +0 -0
  26. /package/.agent/skills/riligar-dev-auth-elysia/assets/{server-snippets.ts → server-snippets.js} +0 -0
@@ -0,0 +1,173 @@
1
+ # Queries — Drizzle
2
+
3
+ ## Imports Comuns
4
+
5
+ ```javascript
6
+ import { db } from '../database/db'
7
+ import { users, posts } from '../database/schema'
8
+ import { eq, and, or, desc, asc } from 'drizzle-orm'
9
+ ```
10
+
11
+ ## Select
12
+
13
+ ### Básico
14
+
15
+ ```javascript
16
+ // Todos os registros
17
+ const allUsers = await db.select().from(users)
18
+
19
+ // Campos específicos
20
+ const names = await db.select({
21
+ id: users.id,
22
+ name: users.name,
23
+ }).from(users)
24
+ ```
25
+
26
+ ### Filtrar
27
+
28
+ ```javascript
29
+ // Por campo único
30
+ const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1)
31
+
32
+ // Múltiplos filtros (AND)
33
+ const results = await db.select().from(users).where(
34
+ and(
35
+ eq(users.active, true),
36
+ eq(users.plan, 'pro')
37
+ )
38
+ )
39
+
40
+ // OR
41
+ const results = await db.select().from(users).where(
42
+ or(eq(users.id, '1'), eq(users.id, '2'))
43
+ )
44
+ ```
45
+
46
+ ### Ordenar e Paginar
47
+
48
+ ```javascript
49
+ const page = await db.select().from(posts)
50
+ .orderBy(desc(posts.createdAt))
51
+ .limit(10)
52
+ .offset(pageIndex * 10)
53
+ ```
54
+
55
+ ## Insert
56
+
57
+ ```javascript
58
+ // Básico
59
+ await db.insert(users).values({ name: 'Dan', email: 'dan@email.com' })
60
+
61
+ // Com retorno
62
+ const [user] = await db.insert(users)
63
+ .values({ name: 'Dan', email: 'dan@email.com' })
64
+ .returning()
65
+
66
+ // Múltiplos
67
+ await db.insert(users).values([
68
+ { name: 'Dan', email: 'dan@email.com' },
69
+ { name: 'Ana', email: 'ana@email.com' },
70
+ ])
71
+
72
+ // Upsert (conflict handling)
73
+ await db.insert(users)
74
+ .values({ id: '1', name: 'Dan' })
75
+ .onConflictDoUpdate({
76
+ target: users.id,
77
+ set: { name: 'Dan', updatedAt: new Date() },
78
+ })
79
+ ```
80
+
81
+ ## Update
82
+
83
+ ```javascript
84
+ await db.update(users)
85
+ .set({ name: 'Mr. Dan', updatedAt: new Date() })
86
+ .where(eq(users.id, userId))
87
+
88
+ // Com retorno
89
+ const [updated] = await db.update(users)
90
+ .set({ plan: 'pro' })
91
+ .where(eq(users.id, userId))
92
+ .returning()
93
+ ```
94
+
95
+ ## Delete
96
+
97
+ ```javascript
98
+ await db.delete(users).where(eq(users.id, userId))
99
+
100
+ // Com retorno
101
+ const [deleted] = await db.delete(users)
102
+ .where(eq(users.id, userId))
103
+ .returning()
104
+ ```
105
+
106
+ ## Queries com Relações
107
+
108
+ Quando tiver relações definidas no schema, use `db.query` em vez de `db.select`:
109
+
110
+ ```javascript
111
+ // Usuário com todos os posts
112
+ const user = await db.query.users.findOne({
113
+ where: eq(users.id, userId),
114
+ with: {
115
+ posts: true,
116
+ },
117
+ })
118
+
119
+ // Posts com autor
120
+ const posts = await db.query.posts.findMany({
121
+ with: {
122
+ author: true,
123
+ },
124
+ orderBy: desc(posts.createdAt),
125
+ limit: 10,
126
+ })
127
+
128
+ // Nesting profundo
129
+ const user = await db.query.users.findOne({
130
+ where: eq(users.id, userId),
131
+ with: {
132
+ posts: {
133
+ with: {
134
+ comments: true,
135
+ },
136
+ },
137
+ },
138
+ })
139
+ ```
140
+
141
+ ## Padrão de Serviço
142
+
143
+ Sempre encapsule queries em serviços:
144
+
145
+ ```javascript
146
+ // services/user.js
147
+ import { db } from '../database/db'
148
+ import { users } from '../database/schema'
149
+ import { eq } from 'drizzle-orm'
150
+
151
+ export async function getUserById(id) {
152
+ const [user] = await db.select().from(users).where(eq(users.id, id)).limit(1)
153
+ return user ?? null
154
+ }
155
+
156
+ export async function createUser(data) {
157
+ const [user] = await db.insert(users).values(data).returning()
158
+ return user
159
+ }
160
+
161
+ export async function updateUser(id, data) {
162
+ const [user] = await db.update(users)
163
+ .set({ ...data, updatedAt: new Date() })
164
+ .where(eq(users.id, id))
165
+ .returning()
166
+ return user ?? null
167
+ }
168
+
169
+ export async function deleteUser(id) {
170
+ const [user] = await db.delete(users).where(eq(users.id, id)).returning()
171
+ return user ?? null
172
+ }
173
+ ```
@@ -0,0 +1,106 @@
1
+ # Schema — Drizzle SQLite
2
+
3
+ ## Definição Básica
4
+
5
+ ```javascript
6
+ // database/schema.js
7
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
8
+ import { relations } from 'drizzle-orm'
9
+
10
+ export const users = sqliteTable('users', {
11
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
12
+ email: text('email').notNull().unique(),
13
+ name: text('name'),
14
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
15
+ updatedAt: integer('updated_at', { mode: 'timestamp' }),
16
+ })
17
+ ```
18
+
19
+ ## Tipos de Colunas
20
+
21
+ | Tipo | Uso | Exemplo |
22
+ | --- | --- | --- |
23
+ | `text()` | Strings, UUIDs, JSON | `text('name').notNull()` |
24
+ | `integer()` | Números, booleans, timestamps | `integer('age')` |
25
+ | `real()` | Decimais (float) | `real('price')` |
26
+ | `blob()` | Dados binários | `blob('avatar')` |
27
+ | `numeric()` | Valores numéricos precisos | `numeric('amount')` |
28
+
29
+ ### Modes do `integer()`
30
+
31
+ ```javascript
32
+ integer('count') // número
33
+ integer('active', { mode: 'boolean' }) // boolean (0/1)
34
+ integer('created_at', { mode: 'timestamp' }) // Date (segundos)
35
+ integer('created_at', { mode: 'timestamp_ms' }) // Date (milissegundos)
36
+ ```
37
+
38
+ ### JSON via `text()`
39
+
40
+ ```javascript
41
+ text('metadata', { mode: 'json' }) // armazena JSON como text
42
+ ```
43
+
44
+ ## Primary Key
45
+
46
+ ```javascript
47
+ // UUID (padrão RiLiGar)
48
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
49
+
50
+ // Auto-increment (quando necessário)
51
+ id: integer('id').primaryKey({ autoIncrement: true }),
52
+ ```
53
+
54
+ ## Foreign Keys
55
+
56
+ ```javascript
57
+ export const posts = sqliteTable('posts', {
58
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
59
+ authorId: text('author_id').notNull().references(() => users.id),
60
+ title: text('title').notNull(),
61
+ body: text('body'),
62
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
63
+ })
64
+ ```
65
+
66
+ ## Relações
67
+
68
+ Relações são definidas separadamente do schema — são apenas para queries, não afetam o banco:
69
+
70
+ ```javascript
71
+ export const usersRelations = relations(users, ({ many }) => ({
72
+ posts: many(posts),
73
+ }))
74
+
75
+ export const postsRelations = relations(posts, ({ one }) => ({
76
+ author: one(users, {
77
+ fields: [posts.authorId],
78
+ references: [users.id],
79
+ }),
80
+ }))
81
+ ```
82
+
83
+ ### Many-to-Many (via junction table)
84
+
85
+ ```javascript
86
+ export const usersToGroups = sqliteTable('users_to_groups', {
87
+ userId: text('user_id').notNull().references(() => users.id),
88
+ groupId: text('group_id').notNull().references(() => groups.id),
89
+ })
90
+
91
+ export const usersRelations = relations(users, ({ many }) => ({
92
+ groups: many(usersToGroups),
93
+ }))
94
+
95
+ export const groupsRelations = relations(groups, ({ many }) => ({
96
+ users: many(usersToGroups),
97
+ }))
98
+ ```
99
+
100
+ ## Defaults e Constraints
101
+
102
+ ```javascript
103
+ plan: text('plan').default('free'),
104
+ active: integer('active', { mode: 'boolean' }).default(true),
105
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
106
+ ```
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: riligar-dev-backend
2
+ name: riligar-dev-manager
3
3
  description: Elysia backend development patterns for Bun. Use when building APIs, routes, plugins, validation, middleware, and error handling with Elysia framework.
4
4
  ---
5
5
 
@@ -10,10 +10,7 @@ description: Elysia backend development patterns for Bun. Use when building APIs
10
10
  ## Mandatory Guidelines
11
11
 
12
12
  > [!IMPORTANT]
13
- > All work in this skill MUST adhere to:
14
- >
15
- > - @[.agent/skills/riligar-dev-clean-code] (Clean Code Standards)
16
- > - @[.agent/skills/riligar-tech-stack] (Tech Stack - Bun, Elysia, SQLite, Drizzle)
13
+ > All work in this skill MUST adhere to rules em `.agent/rules/` — clean-code, code-style, javascript-only, naming-conventions.
17
14
 
18
15
  ## Quick Reference
19
16
 
@@ -62,6 +59,16 @@ src/
62
59
  └── logger.js # Request logging
63
60
  ```
64
61
 
62
+ ## Dependencies
63
+
64
+ | Pacote | Versão | Descrição |
65
+ |---|---|---|
66
+ | `bun` | latest | Runtime |
67
+ | `elysia` | latest | Framework HTTP |
68
+ | `bun:sqlite` | builtin | SQLite driver |
69
+ | `drizzle-orm` | latest | ORM |
70
+ | `bun:s3` | latest | S3/R2 Storage |
71
+
65
72
  ## Core Patterns
66
73
 
67
74
  ### Route Plugin
@@ -109,9 +116,7 @@ console.log(`Server running at ${app.server?.url}`)
109
116
  | --- | --- |
110
117
  | **Authentication** | @[.agent/skills/riligar-dev-auth-elysia] |
111
118
  | **Database** | @[.agent/skills/riligar-dev-database] |
112
- | **Tech Stack** | @[.agent/skills/riligar-tech-stack] |
113
- | **Clean Code** | @[.agent/skills/riligar-dev-clean-code] |
114
- | **Infrastructure** | @[.agent/skills/riligar-infrastructure] |
119
+ | **Infrastructure** | @[.agent/skills/riligar-infra-fly] |
115
120
 
116
121
  ## Decision Checklist
117
122
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: riligar-dev-landing-page
2
+ name: riligar-dev-website
3
3
  description: Specialist in High-Conversion Landing Pages using RiLiGar Design System. Use for: (1) Creating marketing/sales pages, (2) Structuring conversion flows (AIDA/PAS), (3) Implementing high-trust components (Hero, Social Proof, Pricing), (4) Writing persuasive copy.
4
4
  ---
5
5
 
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: riligar-dev-seo
2
+ name: riligar-dev-website-seo
3
3
  description: Implementação de infraestrutura de SEO técnico seguindo a stack RiLiGar (React, Vite, Bun, Elysia). Use para configurar sitemaps, robots.txt, meta tags, OpenGraph, dados estruturados (JSON-LD) e URLs canônicas.
4
4
  ---
5
5
 
@@ -0,0 +1,350 @@
1
+ ---
2
+ name: riligar-infra-cloudfare
3
+ description: "Setup domains in Cloudflare with DNS, email routing, and R2 storage. Use when adding new domains, configuring DNS records, or setting up email redirects."
4
+ ---
5
+
6
+ # Cloudflare Setup
7
+
8
+ Automate Cloudflare workflows: DNS setup, email routing, and R2 storage.
9
+
10
+ ## Prerequisites
11
+
12
+ ### Authentication (Choose One)
13
+
14
+ **Option 1: API Token (Recommended)**
15
+ ```bash
16
+ # Add to .env.local
17
+ CLOUDFLARE_API_TOKEN="your-api-token"
18
+ CLOUDFLARE_ACCOUNT_ID="your-account-id"
19
+ ```
20
+
21
+ Create token at: https://dash.cloudflare.com/profile/api-tokens
22
+ Required permissions:
23
+ - Zone:DNS:Edit
24
+ - Zone:Zone:Read
25
+ - Email Routing Addresses:Edit
26
+ - Email Routing Rules:Edit
27
+ - Account:R2:Edit (for R2 storage)
28
+
29
+ **Option 2: Wrangler CLI**
30
+ ```bash
31
+ # Install wrangler
32
+ bun add -g wrangler
33
+
34
+ # Login (opens browser)
35
+ wrangler login
36
+
37
+ # Verify
38
+ wrangler whoami
39
+ ```
40
+
41
+ ## Workflow
42
+
43
+ When setting up a new domain, follow these steps:
44
+
45
+ ### Step 1: Gather Information
46
+
47
+ Ask the user for:
48
+ 1. **Domain name** (e.g., `example.com`)
49
+ 2. **Email addresses** to create (e.g., `contact`, `support`)
50
+ 3. **Redirect target email** (e.g., `me@gmail.com`)
51
+
52
+ ### Step 2: Get Zone ID
53
+
54
+ ```bash
55
+ # If using API token
56
+ curl -X GET "https://api.cloudflare.com/client/v4/zones?name=DOMAIN" \
57
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
58
+ -H "Content-Type: application/json" | jq '.result[0].id'
59
+
60
+ # If using wrangler
61
+ wrangler pages project list # Shows associated zones
62
+ ```
63
+
64
+ ### Step 3: Setup Email Routing
65
+
66
+ First, enable email routing for the zone (do this in Cloudflare dashboard first time).
67
+
68
+ Then create routing rules:
69
+
70
+ ```bash
71
+ # Create destination address (must be verified first)
72
+ curl -X POST "https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/email/routing/addresses" \
73
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
74
+ -H "Content-Type: application/json" \
75
+ --data '{
76
+ "email": "your-main-email@gmail.com"
77
+ }'
78
+
79
+ # Create routing rule for contact@domain.com
80
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/email/routing/rules" \
81
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
82
+ -H "Content-Type: application/json" \
83
+ --data '{
84
+ "name": "Forward contact",
85
+ "enabled": true,
86
+ "matchers": [{"type": "literal", "field": "to", "value": "contact@DOMAIN"}],
87
+ "actions": [{"type": "forward", "value": ["your-main-email@gmail.com"]}]
88
+ }'
89
+ ```
90
+
91
+ Required MX records for email routing:
92
+ ```bash
93
+ # MX records for Cloudflare Email Routing
94
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
95
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
96
+ -H "Content-Type: application/json" \
97
+ --data '{
98
+ "type": "MX",
99
+ "name": "@",
100
+ "content": "route1.mx.cloudflare.net",
101
+ "priority": 69,
102
+ "ttl": 1
103
+ }'
104
+
105
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
106
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
107
+ -H "Content-Type: application/json" \
108
+ --data '{
109
+ "type": "MX",
110
+ "name": "@",
111
+ "content": "route2.mx.cloudflare.net",
112
+ "priority": 46,
113
+ "ttl": 1
114
+ }'
115
+
116
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
117
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
118
+ -H "Content-Type: application/json" \
119
+ --data '{
120
+ "type": "MX",
121
+ "name": "@",
122
+ "content": "route3.mx.cloudflare.net",
123
+ "priority": 89,
124
+ "ttl": 1
125
+ }'
126
+
127
+ # TXT record for SPF
128
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
129
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
130
+ -H "Content-Type: application/json" \
131
+ --data '{
132
+ "type": "TXT",
133
+ "name": "@",
134
+ "content": "v=spf1 include:_spf.mx.cloudflare.net ~all",
135
+ "ttl": 1
136
+ }'
137
+ ```
138
+
139
+ ### Step 4: Verification Checklist
140
+
141
+ After setup, verify:
142
+
143
+ ```bash
144
+ # List all DNS records
145
+ curl -X GET "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
146
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq '.result[] | {type, name, content}'
147
+
148
+ # Test email routing (send test email to contact@DOMAIN)
149
+ ```
150
+
151
+ ## Interactive Prompts Template
152
+
153
+ When running `/cloudflare`, ask:
154
+
155
+ ```
156
+ What domain are you setting up?
157
+ > example.com
158
+
159
+ What email addresses should I create? (comma-separated)
160
+ > contact, support, hello
161
+
162
+ What email should these redirect to?
163
+ > myemail@gmail.com
164
+ ```
165
+
166
+ ## Common DNS Record Types
167
+
168
+ | Type | Use Case | Proxied |
169
+ |------|----------|---------|
170
+ | A | Root domain to IP | Yes (production) |
171
+ | CNAME | Subdomain to hostname | Yes (production) |
172
+ | TXT | Verification, SPF | N/A |
173
+ | MX | Email routing | N/A |
174
+
175
+ ## Troubleshooting
176
+
177
+ | Issue | Solution |
178
+ |-------|----------|
179
+ | Zone not found | Domain must be added to Cloudflare first |
180
+ | DNS propagation slow | Wait 5-10 minutes, check with `dig` |
181
+ | Email not forwarding | Verify destination email first |
182
+
183
+ ## Useful Commands
184
+
185
+ ```bash
186
+ # Check DNS propagation
187
+ dig DOMAIN +short
188
+ dig DOMAIN MX +short
189
+ dig DOMAIN TXT +short
190
+
191
+ # List zones in account
192
+ curl -X GET "https://api.cloudflare.com/client/v4/zones" \
193
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq '.result[] | {name, id}'
194
+
195
+ # Delete a DNS record
196
+ curl -X DELETE "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records/RECORD_ID" \
197
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
198
+ ```
199
+
200
+ ---
201
+
202
+ # R2 Storage Setup
203
+
204
+ Setup R2 buckets for file storage: user uploads, static assets, backups.
205
+
206
+ ## R2 Workflow
207
+
208
+ ### Step 1: Determine Use Case
209
+
210
+ Ask the user:
211
+ ```
212
+ What do you want to do with R2?
213
+ 1. Create new bucket (full setup)
214
+ 2. Configure existing bucket (CORS, public access)
215
+ 3. Setup custom domain for bucket
216
+ ```
217
+
218
+ ### Step 2: Gather Bucket Info
219
+
220
+ ```
221
+ Bucket name?
222
+ > my-app-uploads
223
+
224
+ What will this bucket store?
225
+ 1. User uploads (images, files) - needs CORS + presigned URLs
226
+ 2. Static assets (public CDN) - needs public access
227
+ 3. Backups (private) - no public access
228
+ > 1
229
+
230
+ Custom domain? (optional, press enter to skip)
231
+ > uploads.myapp.com
232
+ ```
233
+
234
+ ### Step 3: Create Bucket
235
+
236
+ ```bash
237
+ # Create bucket via wrangler
238
+ wrangler r2 bucket create my-app-uploads
239
+
240
+ # Or via API
241
+ curl -X PUT "https://api.cloudflare.com/client/v4/accounts/{account_id}/r2/buckets" \
242
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
243
+ -H "Content-Type: application/json" \
244
+ --data '{"name": "my-app-uploads", "locationHint": "wnam"}'
245
+ ```
246
+
247
+ ### Step 4: Configure CORS (for user uploads)
248
+
249
+ Create `cors.json`:
250
+ ```json
251
+ {
252
+ "corsRules": [
253
+ {
254
+ "allowedOrigins": ["https://myapp.com", "http://localhost:3000"],
255
+ "allowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
256
+ "allowedHeaders": ["*"],
257
+ "exposeHeaders": ["ETag", "Content-Length"],
258
+ "maxAgeSeconds": 3600
259
+ }
260
+ ]
261
+ }
262
+ ```
263
+
264
+ Apply CORS:
265
+ ```bash
266
+ wrangler r2 bucket cors put my-app-uploads --file=cors.json
267
+ ```
268
+
269
+ ### Step 5: Setup Public Access (for static assets)
270
+
271
+ Option A: Enable R2.dev subdomain (via dashboard)
272
+ - Go to R2 > Bucket > Settings > Public access
273
+
274
+ Option B: Custom domain:
275
+ ```bash
276
+ # Add CNAME record
277
+ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
278
+ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
279
+ -H "Content-Type: application/json" \
280
+ --data '{
281
+ "type": "CNAME",
282
+ "name": "uploads",
283
+ "content": "{account_id}.r2.cloudflarestorage.com",
284
+ "ttl": 1,
285
+ "proxied": true
286
+ }'
287
+ ```
288
+
289
+ Then enable custom domain in R2 bucket settings.
290
+
291
+ ### Step 6: Generate S3 API Credentials (for SDK access)
292
+
293
+ 1. Go to R2 > Manage R2 API Tokens
294
+ 2. Create token with Object Read & Write
295
+ 3. Add to `.env.local`:
296
+
297
+ ```bash
298
+ R2_ACCESS_KEY_ID="your-access-key"
299
+ R2_SECRET_ACCESS_KEY="your-secret-key"
300
+ R2_ENDPOINT="https://{account_id}.r2.cloudflarestorage.com"
301
+ R2_BUCKET_NAME="my-app-uploads"
302
+ ```
303
+
304
+ ## R2 Quick Commands
305
+
306
+ ```bash
307
+ # List buckets
308
+ wrangler r2 bucket list
309
+
310
+ # Create bucket
311
+ wrangler r2 bucket create BUCKET_NAME
312
+
313
+ # Delete bucket
314
+ wrangler r2 bucket delete BUCKET_NAME
315
+
316
+ # List objects
317
+ wrangler r2 object list BUCKET_NAME
318
+
319
+ # Upload file
320
+ wrangler r2 object put BUCKET_NAME/path/file.png --file=./local.png
321
+
322
+ # View CORS config
323
+ wrangler r2 bucket cors get BUCKET_NAME
324
+ ```
325
+
326
+ ## R2 Use Case Presets
327
+
328
+ | Use Case | CORS | Public | Custom Domain |
329
+ |----------|------|--------|---------------|
330
+ | User uploads | Yes | No | Optional |
331
+ | Static assets/CDN | No | Yes | Recommended |
332
+ | Backups | No | No | No |
333
+ | Public downloads | No | Yes | Optional |
334
+
335
+ ## R2 Troubleshooting
336
+
337
+ | Issue | Solution |
338
+ |-------|----------|
339
+ | CORS error in browser | Add domain to allowedOrigins |
340
+ | 403 Forbidden | Check API token has R2:Edit permission |
341
+ | Custom domain not working | Ensure CNAME is proxied (orange cloud) |
342
+ | Upload fails | Verify Content-Type header matches file |
343
+
344
+ ---
345
+
346
+ ## References
347
+
348
+ - **[Cloudflare API Reference](references/cloudflare-api.md)**: Auth tokens, zone operations, DNS endpoints
349
+ - **[Email Routing](references/email-routing.md)**: Email forwarding setup and configuration
350
+ - **[R2 Storage](references/r2-storage.md)**: Bucket management, CORS, presigned URLs