@riligar/agents-kit 1.16.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.
- package/.agent/skills/riligar-design-system/references/design-system.md +0 -7
- package/.agent/skills/riligar-dev-database/SKILL.md +45 -0
- package/.agent/skills/riligar-dev-database/references/connection.md +74 -0
- package/.agent/skills/riligar-dev-database/references/migrations.md +70 -0
- package/.agent/skills/riligar-dev-database/references/queries.md +173 -0
- package/.agent/skills/riligar-dev-database/references/schema.md +106 -0
- package/.agent/skills/riligar-dev-manager/SKILL.md +1 -0
- package/.agent/skills/riligar-infra-cloudfare/SKILL.md +8 -95
- package/.agent/skills/riligar-marketing-email/SKILL.md +0 -6
- package/.agent/skills/riligar-marketing-seo/SKILL.md +1 -7
- package/package.json +1 -1
|
@@ -104,10 +104,3 @@ Ao pedir código ou design para uma IA, use este formato:
|
|
|
104
104
|
- **Modais:** Devem ter um backdrop com blur (`backdrop-blur-sm`) e fundo branco sólido. Sem cabeçalhos coloridos.
|
|
105
105
|
- **Dashboards:** Devem privilegiar números grandes (Big Numbers) em monocromia, com rótulos pequenos abaixo. Gráficos devem ser limpos, sem grid lines pesadas.
|
|
106
106
|
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
## 🤖 Integração com Gemini CLI
|
|
110
|
-
|
|
111
|
-
Este repositório possui uma **Skill do Gemini CLI** configurada em [`riligar-design-system/SKILL.md`](../SKILL.md). Ao usar o Gemini CLI neste repositório, o agente aplicará automaticamente estas diretrizes a qualquer código ou design gerado.
|
|
112
|
-
|
|
113
|
-
Para saber como usar, veja o [README principal](../../README.md).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: riligar-dev-database
|
|
3
|
+
description: Database patterns for RiLiGar using Drizzle ORM + bun:sqlite. Use when setting up database connections, defining schemas, creating migrations, or writing queries. Covers SQLite on Fly.io volumes with the drizzle-kit workflow.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Database — Drizzle + bun:sqlite
|
|
7
|
+
|
|
8
|
+
> SQLite nativo no Bun. Zero drivers externos. Base de dados no volume do Fly.io (`/app/data`).
|
|
9
|
+
|
|
10
|
+
## Referências
|
|
11
|
+
|
|
12
|
+
| Arquivo | Quando usar |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| [connection.md](references/connection.md) | Setup inicial: instalação, db.js, drizzle.config |
|
|
15
|
+
| [schema.md](references/schema.md) | Definir tabelas, tipos de colunas, relações |
|
|
16
|
+
| [migrations.md](references/migrations.md) | Criar e executar migrations com drizzle-kit |
|
|
17
|
+
| [queries.md](references/queries.md) | Select, insert, update, delete, queries com relações |
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// database/db.js
|
|
23
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
|
24
|
+
import Database from 'bun:sqlite'
|
|
25
|
+
|
|
26
|
+
const sqlite = new Database(process.env.DB_PATH ?? './data/database.db')
|
|
27
|
+
const db = drizzle({ client: sqlite })
|
|
28
|
+
|
|
29
|
+
export { db }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Regras
|
|
33
|
+
|
|
34
|
+
- **Caminho do banco:** `/app/data/database.db` em produção (volume Fly.io). `./data/database.db` em desenvolvimento.
|
|
35
|
+
- **Migrations sempre:** Use `drizzle-kit generate` + `drizzle-kit migrate`. Nunca edite migrations à mão.
|
|
36
|
+
- **Schema único:** Todas as tabelas em `database/schema.js`.
|
|
37
|
+
- **Migrations no startup:** Use `migrate()` no `index.js` antes de `.listen()`.
|
|
38
|
+
|
|
39
|
+
## Related Skills
|
|
40
|
+
|
|
41
|
+
| Need | Skill |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| **Backend (Elysia)** | @[.agent/skills/riligar-dev-manager] |
|
|
44
|
+
| **Payments (Stripe)** | @[.agent/skills/riligar-infra-stripe] |
|
|
45
|
+
| **Infrastructure** | @[.agent/skills/riligar-infra-fly] |
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Database Connection
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun add drizzle-orm
|
|
7
|
+
bun add -d drizzle-kit
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
No additional drivers needed — `bun:sqlite` is built into Bun.
|
|
11
|
+
|
|
12
|
+
## db.js
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// database/db.js
|
|
16
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite'
|
|
17
|
+
import Database from 'bun:sqlite'
|
|
18
|
+
|
|
19
|
+
const sqlite = new Database(process.env.DB_PATH ?? './data/database.db')
|
|
20
|
+
const db = drizzle({ client: sqlite })
|
|
21
|
+
|
|
22
|
+
export { db }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- Em **desenvolvimento**: `DB_PATH` não é definido → usa `./data/database.db`
|
|
26
|
+
- Em **produção** (Fly.io): `fly secrets set DB_PATH=/app/data/database.db`
|
|
27
|
+
|
|
28
|
+
## drizzle.config.js
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
// drizzle.config.js (na raiz do projeto)
|
|
32
|
+
import { defineConfig } from 'drizzle-kit'
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
dialect: 'sqlite',
|
|
36
|
+
schema: './database/schema.js',
|
|
37
|
+
out: './database/migrations',
|
|
38
|
+
dbCredentials: {
|
|
39
|
+
url: process.env.DB_PATH ?? './data/database.db',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Migrations no Startup
|
|
45
|
+
|
|
46
|
+
No `index.js`, execute migrations antes de iniciar o servidor:
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
// index.js
|
|
50
|
+
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
|
51
|
+
import { db } from './database/db'
|
|
52
|
+
|
|
53
|
+
await migrate(db, { migrationsFolder: './database/migrations' })
|
|
54
|
+
|
|
55
|
+
// ... resto do setup
|
|
56
|
+
const app = new Elysia()
|
|
57
|
+
.use(routes)
|
|
58
|
+
.listen(3000)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Isso garante que o banco esteja sempre atualizado quando o servidor inicia no Fly.io.
|
|
62
|
+
|
|
63
|
+
## Estrutura de Arquivos
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
database/
|
|
67
|
+
├── db.js # Conexão (drizzle + bun:sqlite)
|
|
68
|
+
├── schema.js # Todas as tabelas e relações
|
|
69
|
+
└── migrations/
|
|
70
|
+
├── 0000_*.sql # Migrations geradas pelo drizzle-kit
|
|
71
|
+
├── 0001_*.sql
|
|
72
|
+
└── meta/
|
|
73
|
+
└── _journal.json
|
|
74
|
+
```
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Migrations — drizzle-kit
|
|
2
|
+
|
|
3
|
+
## Fluxo Padrão
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
1. Edite database/schema.js
|
|
7
|
+
2. bun run db:generate → gera migration SQL
|
|
8
|
+
3. bun run db:migrate → aplica no banco
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Scripts no package.json
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
{
|
|
15
|
+
"scripts": {
|
|
16
|
+
"db:generate": "drizzle-kit generate",
|
|
17
|
+
"db:migrate": "drizzle-kit migrate",
|
|
18
|
+
"db:push": "drizzle-kit push",
|
|
19
|
+
"db:studio": "drizzle-kit studio"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Comandos
|
|
25
|
+
|
|
26
|
+
### Gerar migration
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bun run db:generate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Compara o schema atual com o último snapshot. Gera um arquivo `.sql` em `database/migrations/`. Pergunta se algum campo foi renomeado (vs criado novo + deletado).
|
|
33
|
+
|
|
34
|
+
### Aplicar migrations
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun run db:migrate
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Lê as migrations não aplicadas, executa em ordem. Mantém histórico no banco.
|
|
41
|
+
|
|
42
|
+
### Push (apenas desenvolvimento)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bun run db:push
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Aplica mudanças diretamente no banco sem gerar arquivos de migration. Útil durante prototipage rápido. **Nunca use em produção.**
|
|
49
|
+
|
|
50
|
+
### Studio (GUI)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bun run db:studio
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Abre uma interface visual para explorar e editar dados no banco.
|
|
57
|
+
|
|
58
|
+
## Migrations Programáticas (Startup)
|
|
59
|
+
|
|
60
|
+
Em produção, as migrations são executadas automaticamente no startup da aplicação:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// index.js
|
|
64
|
+
import { migrate } from 'drizzle-orm/bun-sqlite/migrator'
|
|
65
|
+
import { db } from './database/db'
|
|
66
|
+
|
|
67
|
+
await migrate(db, { migrationsFolder: './database/migrations' })
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Isso garante que o banco esteja sempre atualizado quando o servidor inicia no Fly.io.
|
|
@@ -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
|
+
```
|
|
@@ -115,6 +115,7 @@ console.log(`Server running at ${app.server?.url}`)
|
|
|
115
115
|
| Need | Skill |
|
|
116
116
|
| --- | --- |
|
|
117
117
|
| **Authentication** | @[.agent/skills/riligar-dev-auth-elysia] |
|
|
118
|
+
| **Database** | @[.agent/skills/riligar-dev-database] |
|
|
118
119
|
| **Infrastructure** | @[.agent/skills/riligar-infra-fly] |
|
|
119
120
|
|
|
120
121
|
## Decision Checklist
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: riligar-infra-cloudfare
|
|
3
|
-
description: "Setup domains in Cloudflare with DNS
|
|
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
4
|
---
|
|
5
5
|
|
|
6
6
|
# Cloudflare Setup
|
|
7
7
|
|
|
8
|
-
Automate Cloudflare workflows: DNS setup,
|
|
8
|
+
Automate Cloudflare workflows: DNS setup, email routing, and R2 storage.
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
@@ -38,13 +38,6 @@ wrangler login
|
|
|
38
38
|
wrangler whoami
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
### Other Tools
|
|
42
|
-
```bash
|
|
43
|
-
# Vercel CLI (required)
|
|
44
|
-
bun add -g vercel
|
|
45
|
-
vercel login
|
|
46
|
-
```
|
|
47
|
-
|
|
48
41
|
## Workflow
|
|
49
42
|
|
|
50
43
|
When setting up a new domain, follow these steps:
|
|
@@ -53,10 +46,8 @@ When setting up a new domain, follow these steps:
|
|
|
53
46
|
|
|
54
47
|
Ask the user for:
|
|
55
48
|
1. **Domain name** (e.g., `example.com`)
|
|
56
|
-
2. **
|
|
57
|
-
3. **
|
|
58
|
-
4. **Email addresses** to create (e.g., `contact`, `support`)
|
|
59
|
-
5. **Redirect target email** (e.g., `me@gmail.com`)
|
|
49
|
+
2. **Email addresses** to create (e.g., `contact`, `support`)
|
|
50
|
+
3. **Redirect target email** (e.g., `me@gmail.com`)
|
|
60
51
|
|
|
61
52
|
### Step 2: Get Zone ID
|
|
62
53
|
|
|
@@ -70,74 +61,7 @@ curl -X GET "https://api.cloudflare.com/client/v4/zones?name=DOMAIN" \
|
|
|
70
61
|
wrangler pages project list # Shows associated zones
|
|
71
62
|
```
|
|
72
63
|
|
|
73
|
-
### Step 3:
|
|
74
|
-
|
|
75
|
-
Clerk provides specific DNS records for each project. Common patterns:
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
# Example: CNAME record
|
|
79
|
-
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
|
|
80
|
-
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
|
81
|
-
-H "Content-Type: application/json" \
|
|
82
|
-
--data '{
|
|
83
|
-
"type": "CNAME",
|
|
84
|
-
"name": "clerk",
|
|
85
|
-
"content": "frontend-api.clerk.dev",
|
|
86
|
-
"ttl": 1,
|
|
87
|
-
"proxied": false
|
|
88
|
-
}'
|
|
89
|
-
|
|
90
|
-
# Example: TXT record for verification
|
|
91
|
-
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
|
|
92
|
-
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
|
93
|
-
-H "Content-Type: application/json" \
|
|
94
|
-
--data '{
|
|
95
|
-
"type": "TXT",
|
|
96
|
-
"name": "@",
|
|
97
|
-
"content": "clerk-verification=xxxxx",
|
|
98
|
-
"ttl": 1
|
|
99
|
-
}'
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Step 4: Add Domain to Vercel
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
# Add domain to Vercel project
|
|
106
|
-
vercel domains add DOMAIN --scope=TEAM_SLUG
|
|
107
|
-
|
|
108
|
-
# Or link to specific project
|
|
109
|
-
vercel domains add DOMAIN PROJECT_NAME
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Then create Vercel DNS records:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
# A record for root domain
|
|
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": "A",
|
|
121
|
-
"name": "@",
|
|
122
|
-
"content": "76.76.21.21",
|
|
123
|
-
"ttl": 1,
|
|
124
|
-
"proxied": false
|
|
125
|
-
}'
|
|
126
|
-
|
|
127
|
-
# CNAME for www subdomain
|
|
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": "CNAME",
|
|
133
|
-
"name": "www",
|
|
134
|
-
"content": "cname.vercel-dns.com",
|
|
135
|
-
"ttl": 1,
|
|
136
|
-
"proxied": false
|
|
137
|
-
}'
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Step 5: Setup Email Routing
|
|
64
|
+
### Step 3: Setup Email Routing
|
|
141
65
|
|
|
142
66
|
First, enable email routing for the zone (do this in Cloudflare dashboard first time).
|
|
143
67
|
|
|
@@ -212,7 +136,7 @@ curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
|
|
|
212
136
|
}'
|
|
213
137
|
```
|
|
214
138
|
|
|
215
|
-
### Step
|
|
139
|
+
### Step 4: Verification Checklist
|
|
216
140
|
|
|
217
141
|
After setup, verify:
|
|
218
142
|
|
|
@@ -221,9 +145,6 @@ After setup, verify:
|
|
|
221
145
|
curl -X GET "https://api.cloudflare.com/client/v4/zones/ZONE_ID/dns_records" \
|
|
222
146
|
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" | jq '.result[] | {type, name, content}'
|
|
223
147
|
|
|
224
|
-
# Check Vercel domain status
|
|
225
|
-
vercel domains inspect DOMAIN
|
|
226
|
-
|
|
227
148
|
# Test email routing (send test email to contact@DOMAIN)
|
|
228
149
|
```
|
|
229
150
|
|
|
@@ -235,12 +156,6 @@ When running `/cloudflare`, ask:
|
|
|
235
156
|
What domain are you setting up?
|
|
236
157
|
> example.com
|
|
237
158
|
|
|
238
|
-
Paste the Clerk DNS records from your Clerk dashboard:
|
|
239
|
-
> [user pastes records]
|
|
240
|
-
|
|
241
|
-
What's the Vercel project name?
|
|
242
|
-
> my-saas-app
|
|
243
|
-
|
|
244
159
|
What email addresses should I create? (comma-separated)
|
|
245
160
|
> contact, support, hello
|
|
246
161
|
|
|
@@ -252,8 +167,8 @@ What email should these redirect to?
|
|
|
252
167
|
|
|
253
168
|
| Type | Use Case | Proxied |
|
|
254
169
|
|------|----------|---------|
|
|
255
|
-
| A | Root domain to IP |
|
|
256
|
-
| CNAME | Subdomain to hostname |
|
|
170
|
+
| A | Root domain to IP | Yes (production) |
|
|
171
|
+
| CNAME | Subdomain to hostname | Yes (production) |
|
|
257
172
|
| TXT | Verification, SPF | N/A |
|
|
258
173
|
| MX | Email routing | N/A |
|
|
259
174
|
|
|
@@ -264,8 +179,6 @@ What email should these redirect to?
|
|
|
264
179
|
| Zone not found | Domain must be added to Cloudflare first |
|
|
265
180
|
| DNS propagation slow | Wait 5-10 minutes, check with `dig` |
|
|
266
181
|
| Email not forwarding | Verify destination email first |
|
|
267
|
-
| Vercel 404 | Check DNS proxied=false for Vercel records |
|
|
268
|
-
| Clerk verification failed | Ensure TXT record is on root (@) |
|
|
269
182
|
|
|
270
183
|
## Useful Commands
|
|
271
184
|
|
|
@@ -1043,9 +1043,3 @@ If you need more context:
|
|
|
1043
1043
|
|
|
1044
1044
|
---
|
|
1045
1045
|
|
|
1046
|
-
## Related Skills
|
|
1047
|
-
|
|
1048
|
-
- **onboarding-cro**: For in-app onboarding (email supports this)
|
|
1049
|
-
- **copywriting**: For landing pages emails link to
|
|
1050
|
-
- **ab-test-setup**: For testing email elements
|
|
1051
|
-
- **popup-cro**: For email capture popups
|
|
@@ -33,7 +33,7 @@ The Feasibility Index answers one question:
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Programmatic SEO Feasibility Index
|
|
37
37
|
|
|
38
38
|
### Total Score: **0–100**
|
|
39
39
|
|
|
@@ -337,9 +337,3 @@ If triggered, **halt indexing or roll back**:
|
|
|
337
337
|
|
|
338
338
|
---
|
|
339
339
|
|
|
340
|
-
## Related Skills
|
|
341
|
-
|
|
342
|
-
- **seo-audit** – Audit programmatic pages post-launch
|
|
343
|
-
- **schema-markup** – Add structured data to templates
|
|
344
|
-
- **copywriting** – Improve non-templated sections
|
|
345
|
-
- **analytics-tracking** – Measure performance and validate value
|