ilumin-cli 1.0.1 → 1.1.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.
@@ -0,0 +1,425 @@
1
+ # Ilumin Compose Formatter
2
+
3
+ This skill defines the mandatory rules and patterns for writing `docker-compose.yml` files compatible with the Ilumin Cloud platform. Follow every rule — small mistakes like a fixed router name or a wrong network definition are the most common causes of deployment failures.
4
+
5
+ ---
6
+
7
+ ## How Ilumin's Infrastructure Works
8
+
9
+ Understanding the infrastructure prevents 90% of errors:
10
+
11
+ ```
12
+ [Internet] → [Traefik v3 on port 80/443] → [Docker containers via labels]
13
+
14
+ Reads routing rules from labels
15
+ Handles SSL via Let's Encrypt
16
+ Routes by hostname or path
17
+ ```
18
+
19
+ - Traefik runs as a separate container on the `traefik` **external** Docker network.
20
+ - All app containers that need public access must join the `traefik` network.
21
+ - Containers that must stay private (databases, caches) must join only the `internal` network.
22
+ - Containers on the same `internal` network communicate using the **service name** as hostname (e.g., `postgres:5432`).
23
+
24
+ ---
25
+
26
+ ## Mandatory Formatting Rules
27
+
28
+ ### 1. No Comments
29
+ Remove **all** comments (lines starting with `#`) from the final compose file.
30
+
31
+ ### 2. No Resource Limits
32
+ Remove any `deploy`, `resources`, `limits`, or `reservations` sections — Ilumin manages these at the infrastructure level.
33
+
34
+ ### 3. Image Version Variable
35
+ Replace the image tag of the **main application** with `${APP_VERSION}`.
36
+
37
+ ```yml
38
+ # ❌ Wrong
39
+ image: n8n:latest
40
+ image: ghost:5.82.2
41
+ image: myapp:v2.1.0
42
+
43
+ # ✅ Correct
44
+ image: n8n:${APP_VERSION}
45
+ image: ghost:${APP_VERSION}
46
+ image: myapp:${APP_VERSION}
47
+ ```
48
+
49
+ > **Critical:** `APP_VERSION` replaces the **entire tag** after the colon, including any `v` prefix. Never write `v${APP_VERSION}`.
50
+
51
+ ### 4. Database Password Variable
52
+ Replace **all** database passwords with `${DB_PASSWORD}`:
53
+
54
+ ```yml
55
+ # Applies to: POSTGRES_PASSWORD, MYSQL_ROOT_PASSWORD, MYSQL_PASSWORD, MARIADB_ROOT_PASSWORD, etc.
56
+ - POSTGRES_PASSWORD=${DB_PASSWORD}
57
+ - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
58
+ - MYSQL_PASSWORD=${DB_PASSWORD}
59
+ ```
60
+
61
+ ### 5. Optional Admin Credentials
62
+ If the app supports initial admin setup, use these standard variables:
63
+ - `${APP_USERNAME}` — admin username
64
+ - `${APP_PASSWORD}` — admin password
65
+ - `${APP_EMAIL}` — admin email
66
+
67
+ ### 6. No Backslashes in URLs
68
+ Never use `\` before `$` in URLs inside the compose file.
69
+
70
+ ```yml
71
+ # ❌ Wrong
72
+ - APP_URL=https://\${BASE_DOMAIN}
73
+
74
+ # ✅ Correct
75
+ - APP_URL=https://${BASE_DOMAIN}
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Network Rules (Critical)
81
+
82
+ ```yml
83
+ # ✅ Main app service (public-facing) — both networks
84
+ networks:
85
+ - traefik
86
+ - internal
87
+
88
+ # ✅ Dependency services (db, redis, queue) — internal only
89
+ networks:
90
+ - internal
91
+ ```
92
+
93
+ **Always define networks at the bottom of the file exactly like this:**
94
+
95
+ ```yml
96
+ networks:
97
+ traefik:
98
+ external: true
99
+ internal:
100
+ ```
101
+
102
+ > ⚠️ The `traefik` network must be `external: true`. Forgetting this causes Traefik to be unable to discover the container.
103
+
104
+ ---
105
+
106
+ ## Traefik Labels (Critical)
107
+
108
+ Every public-facing service needs these labels. **Copy this block exactly** and replace `<appname>` with a unique identifier for the app.
109
+
110
+ ```yml
111
+ labels:
112
+ - traefik.enable=true
113
+ - traefik.docker.network=traefik
114
+ - traefik.http.routers.<appname>.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
115
+ - traefik.http.routers.<appname>.entrypoints=websecure
116
+ - traefik.http.routers.<appname>.tls=true
117
+ - traefik.http.routers.<appname>.tls.certresolver=letsencrypt
118
+ - traefik.http.services.<appname>.loadbalancer.server.port=<PORT>
119
+ ```
120
+
121
+ ### Router Naming Rules (Most Common Error)
122
+
123
+ The router/service name in Traefik labels **must be unique across all apps on the server**. Using generic names like `backend` or `frontend` causes conflicts when multiple apps are deployed.
124
+
125
+ **Formula:** `{appname}{role}` — always prefix with the app name.
126
+
127
+ ```yml
128
+ # ❌ WRONG — will conflict with any other app using the same generic name
129
+ - traefik.http.routers.backend.rule=...
130
+ - traefik.http.routers.frontend.rule=...
131
+
132
+ # ✅ CORRECT — unique per app
133
+ - traefik.http.routers.n8nmain.rule=...
134
+ - traefik.http.routers.ghostblog.rule=...
135
+ - traefik.http.routers.myappfrontend.rule=...
136
+ - traefik.http.routers.myappbackend.rule=...
137
+ ```
138
+
139
+ ### Domain Rule Syntax
140
+
141
+ The domain rule must use this exact syntax — it supports both the Ilumin base domain and an optional custom domain:
142
+
143
+ ```
144
+ Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
145
+ ```
146
+
147
+ > **Why this syntax?** `${CUSTOM_DOMAIN:+ || Host(...)}` is a shell parameter expansion that only adds the custom domain rule if the variable is set. This allows the platform to manage domain routing dynamically without requiring a compose file change.
148
+
149
+ ---
150
+
151
+ ## Multi-Service Compose Rules
152
+
153
+ ### Two Services, One Domain (Path Routing — Frontend + Backend)
154
+
155
+ When your app has a frontend and a backend on the **same domain**:
156
+
157
+ ```yml
158
+ # Frontend: answers on / (no path prefix needed)
159
+ labels:
160
+ - traefik.http.routers.myappfrontend.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
161
+ - traefik.http.services.myappfrontend.loadbalancer.server.port=3000
162
+
163
+ # Backend: answers on /api
164
+ labels:
165
+ - traefik.http.routers.myappbackend.rule=(Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}) && PathPrefix(`/api`)
166
+ - traefik.http.services.myappbackend.loadbalancer.server.port=8000
167
+ ```
168
+
169
+ ### Two Services, Two Subdomains
170
+
171
+ When you need separate subdomains for backend and frontend:
172
+
173
+ ```yml
174
+ # Frontend — base domain
175
+ labels:
176
+ - traefik.http.routers.myappfrontend.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
177
+
178
+ # Backend — api subdomain prefix
179
+ labels:
180
+ - traefik.http.routers.myappbackend.rule=Host(`api.${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`api.${CUSTOM_DOMAIN}`)}
181
+
182
+ # Other services follow the same prefix pattern:
183
+ # db.${BASE_DOMAIN}, admin.${BASE_DOMAIN}, etc.
184
+ ```
185
+
186
+ ---
187
+
188
+ ## HTTP → HTTPS Redirect (Optional but Recommended)
189
+
190
+ To redirect HTTP traffic to HTTPS, add these labels alongside the main router:
191
+
192
+ ```yml
193
+ labels:
194
+ # ... (main HTTPS router labels above) ...
195
+
196
+ # HTTP redirect router
197
+ - traefik.http.routers.<appname>-http.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
198
+ - traefik.http.routers.<appname>-http.entrypoints=web
199
+ - traefik.http.routers.<appname>-http.middlewares=redirect-to-https
200
+ - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Architecture Options
206
+
207
+ ### Option 1: Unified Build (Recommended) ✅
208
+
209
+ Serve the compiled frontend through the backend. One container, no CORS issues.
210
+
211
+ - Build the frontend in Stage 1 of the Dockerfile.
212
+ - Copy `dist/` into the backend Stage 2.
213
+ - Backend serves static files from `/` and API from `/api`.
214
+ - Only one Traefik router needed.
215
+
216
+ ### Option 2: Separate Containers
217
+
218
+ Use path routing or subdomains. Requires proper CORS config on the backend.
219
+
220
+ > The browser **cannot** access `http://backend:8000` — the Docker internal network is not accessible from the user's browser. The frontend must call the public domain (e.g., `/api` relative path or `https://api.domain.com`).
221
+
222
+ ---
223
+
224
+ ## Common Errors and Fixes
225
+
226
+ | Error | Cause | Fix |
227
+ |---|---|---|
228
+ | App unreachable (502 Bad Gateway) | Wrong port in `loadbalancer.server.port` | Set the port the container actually listens on |
229
+ | App unreachable (no SSL / cert error) | Missing `tls=true` or `certresolver=letsencrypt` | Add both TLS labels |
230
+ | Two apps conflict (first works, second doesn't) | Duplicate router names in Traefik labels | Use unique names: `{appname}{role}` |
231
+ | Database not reachable from app | DB container not on `internal` network | Ensure both app and DB are on `internal` |
232
+ | Traefik can't discover container | `traefik` network not set as `external: true` | Fix the network definition at the bottom |
233
+ | URL contains `v${APP_VERSION}` | Manually adding `v` before the variable | Remove the `v` — `APP_VERSION` already includes it |
234
+ | URL contains backslash `\$` | Escaping `$` in compose | Remove `\` — dollar signs don't need escaping in compose YAML |
235
+ | Custom domain not working | `manage_domain` not called after install | Call `manage_domain` and configure CNAME at DNS provider |
236
+
237
+ ---
238
+
239
+ ## Reference Examples
240
+
241
+ ### Minimal Single App (Uptime Kuma)
242
+
243
+ ```yml
244
+ services:
245
+ uptime-kuma:
246
+ image: louislam/uptime-kuma:${APP_VERSION}
247
+ volumes:
248
+ - uptime_data:/app/data
249
+ networks:
250
+ - traefik
251
+ - internal
252
+ labels:
253
+ - traefik.enable=true
254
+ - traefik.docker.network=traefik
255
+ - traefik.http.routers.uptimekuma.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
256
+ - traefik.http.routers.uptimekuma.entrypoints=websecure
257
+ - traefik.http.routers.uptimekuma.tls=true
258
+ - traefik.http.routers.uptimekuma.tls.certresolver=letsencrypt
259
+ - traefik.http.services.uptimekuma.loadbalancer.server.port=3001
260
+ restart: unless-stopped
261
+
262
+ volumes:
263
+ uptime_data:
264
+
265
+ networks:
266
+ traefik:
267
+ external: true
268
+ internal:
269
+ ```
270
+
271
+ ### App + Database (WordPress + MySQL)
272
+
273
+ ```yml
274
+ services:
275
+ wordpress:
276
+ image: wordpress:${APP_VERSION}
277
+ depends_on:
278
+ - mysql
279
+ environment:
280
+ - WORDPRESS_DB_HOST=mysql
281
+ - WORDPRESS_DB_USER=wordpress
282
+ - WORDPRESS_DB_PASSWORD=${DB_PASSWORD}
283
+ - WORDPRESS_DB_NAME=wordpress
284
+ volumes:
285
+ - wordpress_data:/var/www/html
286
+ networks:
287
+ - traefik
288
+ - internal
289
+ labels:
290
+ - traefik.enable=true
291
+ - traefik.docker.network=traefik
292
+ - traefik.http.routers.wordpress.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
293
+ - traefik.http.routers.wordpress.entrypoints=websecure
294
+ - traefik.http.routers.wordpress.tls=true
295
+ - traefik.http.routers.wordpress.tls.certresolver=letsencrypt
296
+ - traefik.http.services.wordpress.loadbalancer.server.port=80
297
+ restart: unless-stopped
298
+
299
+ mysql:
300
+ image: mysql:5.7
301
+ environment:
302
+ - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
303
+ - MYSQL_DATABASE=wordpress
304
+ - MYSQL_USER=wordpress
305
+ - MYSQL_PASSWORD=${DB_PASSWORD}
306
+ volumes:
307
+ - mysql_data:/var/lib/mysql
308
+ networks:
309
+ - internal
310
+ restart: unless-stopped
311
+
312
+ volumes:
313
+ wordpress_data:
314
+ mysql_data:
315
+
316
+ networks:
317
+ traefik:
318
+ external: true
319
+ internal:
320
+ ```
321
+
322
+ ### App + Database (Baserow + PostgreSQL)
323
+
324
+ ```yml
325
+ services:
326
+ baserow:
327
+ image: baserow/baserow:${APP_VERSION}
328
+ depends_on:
329
+ - postgres
330
+ volumes:
331
+ - baserow_data:/baserow/data
332
+ environment:
333
+ - BASEROW_PUBLIC_URL=https://${BASE_DOMAIN}
334
+ - DATABASE_HOST=postgres
335
+ - DATABASE_NAME=baserow
336
+ - DATABASE_USER=postgres
337
+ - DATABASE_PASSWORD=${DB_PASSWORD}
338
+ networks:
339
+ - traefik
340
+ - internal
341
+ labels:
342
+ - traefik.enable=true
343
+ - traefik.docker.network=traefik
344
+ - traefik.http.routers.baserow.rule=Host(`${BASE_DOMAIN}`)${CUSTOM_DOMAIN:+ || Host(`${CUSTOM_DOMAIN}`)}
345
+ - traefik.http.routers.baserow.entrypoints=websecure
346
+ - traefik.http.routers.baserow.tls=true
347
+ - traefik.http.routers.baserow.tls.certresolver=letsencrypt
348
+ - traefik.http.routers.baserow.service=baserow
349
+ - traefik.http.services.baserow.loadbalancer.server.port=80
350
+ restart: unless-stopped
351
+
352
+ postgres:
353
+ image: postgres:16
354
+ environment:
355
+ - POSTGRES_USER=postgres
356
+ - POSTGRES_PASSWORD=${DB_PASSWORD}
357
+ - POSTGRES_DB=baserow
358
+ volumes:
359
+ - postgres_data:/var/lib/postgresql/data
360
+ networks:
361
+ - internal
362
+ restart: unless-stopped
363
+
364
+ volumes:
365
+ baserow_data:
366
+ postgres_data:
367
+
368
+ networks:
369
+ traefik:
370
+ external: true
371
+ internal:
372
+ ```
373
+
374
+ ### Custom App (Fixed Domain, No Variables)
375
+
376
+ Used when the user wants to hardcode a specific domain. Inform the user that Ilumin's domain management panel will NOT work in this mode — they must manage DNS manually.
377
+
378
+ ```yml
379
+ services:
380
+ app:
381
+ image: myapp:${APP_VERSION}
382
+ container_name: myapp
383
+ restart: unless-stopped
384
+ environment:
385
+ - NODE_ENV=production
386
+ networks:
387
+ - traefik
388
+ - internal
389
+ labels:
390
+ - traefik.enable=true
391
+ - traefik.docker.network=traefik
392
+ - traefik.http.routers.myapp.rule=Host(`app.mycompany.com`)
393
+ - traefik.http.routers.myapp.entrypoints=websecure
394
+ - traefik.http.routers.myapp.tls=true
395
+ - traefik.http.routers.myapp.tls.certresolver=letsencrypt
396
+ - traefik.http.routers.myapp-http.rule=Host(`app.mycompany.com`)
397
+ - traefik.http.routers.myapp-http.entrypoints=web
398
+ - traefik.http.routers.myapp-http.middlewares=redirect-to-https
399
+ - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
400
+ - traefik.http.services.myapp.loadbalancer.server.port=3000
401
+
402
+ networks:
403
+ traefik:
404
+ external: true
405
+ internal:
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Pre-Deploy Compose Checklist
411
+
412
+ - [ ] All image versions replaced with `${APP_VERSION}` (no `v` prefix before the variable)
413
+ - [ ] All database passwords replaced with `${DB_PASSWORD}`
414
+ - [ ] No hardcoded secrets or credentials in the file
415
+ - [ ] Main service is on both `traefik` and `internal` networks
416
+ - [ ] Database/cache services are on `internal` network only
417
+ - [ ] Network definitions at the bottom: `traefik` is `external: true`, `internal` has no extra config
418
+ - [ ] Traefik router name is unique (format: `{appname}{role}`)
419
+ - [ ] Domain rule uses exact syntax with `${BASE_DOMAIN}` and `${CUSTOM_DOMAIN:+ ...}` expansion
420
+ - [ ] `loadbalancer.server.port` matches the actual port the container listens on
421
+ - [ ] TLS labels present: `tls=true` + `tls.certresolver=letsencrypt`
422
+ - [ ] No `\` backslashes before `$` in URLs
423
+ - [ ] No comments (`#`) in the final file
424
+ - [ ] No `deploy`, `resources`, `limits`, or `reservations` sections
425
+ - [ ] All volumes declared in the `volumes:` section at the bottom
@@ -0,0 +1,179 @@
1
+ # Ilumin Recommended Stack
2
+
3
+ When the user hasn't specified which infrastructure services to use, follow this recommended stack. These services are optimized for Ilumin Cloud — they are lightweight, self-hosted, and integrate well with each other via the `internal` Docker network.
4
+
5
+ ---
6
+
7
+ ## How Internal Communication Works
8
+
9
+ All services in the same `docker-compose.yml` (or on the same `internal` Docker network) can communicate using the **service name as the hostname**.
10
+
11
+ ```
12
+ App Container → [internal network] → postgres:5432
13
+ App Container → [internal network] → redis:6379
14
+ NocoDB → [internal network] → postgres:5432
15
+ ```
16
+
17
+ > The key rule: **both the app and the service it connects to must share the same network** (always `internal`). The public internet never touches these internal services — only the main app container is exposed via Traefik.
18
+
19
+ ---
20
+
21
+ ## 1. 🗄️ Database: NocoDB + PostgreSQL
22
+
23
+ **NocoDB** is the recommended database layer for Ilumin apps. It is a lightweight, self-hosted Airtable alternative that sits on top of PostgreSQL and exposes a full REST + GraphQL API, a visual UI, and programmatic table creation via API — no raw SQL required for most operations.
24
+
25
+ ### Why NocoDB?
26
+ - Full REST API to read/write/create tables from any app or AI agent
27
+ - Visual interface for non-developers to manage data
28
+ - Integrates with webhooks, automations, and third-party tools
29
+ - JWT-based API authentication via `NC_AUTH_JWT_SECRET`
30
+
31
+ ### Connecting Your App to NocoDB
32
+
33
+ NocoDB exposes an HTTP API. Your app does **not** connect to NocoDB as a database directly — it makes API calls to NocoDB's REST endpoint.
34
+
35
+ If your app is in the **same compose** or **same `internal` network**, use the internal hostname:
36
+
37
+ ```bash
38
+ # Internal (same internal network) — preferred
39
+ NOCODB_URL=http://nocodb:8080
40
+
41
+ # External (different server or no shared network)
42
+ NOCODB_URL=https://your-nocodb-domain.com
43
+ ```
44
+
45
+ ```python
46
+ # Python example — connecting via internal network
47
+ import httpx
48
+
49
+ NOCODB_URL = os.getenv("NOCODB_URL", "http://nocodb:8080")
50
+ NOCODB_API_KEY = os.getenv("NOCODB_API_KEY") # = NC_AUTH_JWT_SECRET value
51
+
52
+ headers = {
53
+ "xc-auth": NOCODB_API_KEY,
54
+ "Content-Type": "application/json"
55
+ }
56
+
57
+ # List records from a table
58
+ response = httpx.get(f"{NOCODB_URL}/api/v1/db/data/noco/{{TABLE_ID}}/{{VIEW_ID}}", headers=headers)
59
+ ```
60
+
61
+ ### If NocoDB Is Already Installed
62
+
63
+ Before installing a new NocoDB, check if one is already running on the server:
64
+
65
+ 1. Run `list_servers` to see installed apps.
66
+ 2. If NocoDB is already there, retrieve its `NC_AUTH_JWT_SECRET` env var — this is the API key your app uses to authenticate.
67
+ 3. Use the internal URL `http://nocodb:8080` if your app is in the same compose, or the public domain if separate.
68
+
69
+ ---
70
+
71
+ ## 2. ⚡ Cache & Sessions: Redis + RedisInsight
72
+
73
+ **Redis** is the recommended solution for caching, session storage, and rate limiting. **RedisInsight** is the official Redis visual UI — install them together for observability.
74
+
75
+ ### Connecting Your App to Redis
76
+
77
+ ```bash
78
+ # Internal connection string (same internal network)
79
+ REDIS_URL=redis://redis:6379
80
+ ```
81
+
82
+ ```typescript
83
+ // Node.js / TypeScript — ioredis
84
+ import Redis from 'ioredis';
85
+
86
+ const redis = new Redis(process.env.REDIS_URL || 'redis://redis:6379');
87
+
88
+ // Cache example
89
+ await redis.set('key', 'value', 'EX', 3600); // expires in 1 hour
90
+ const value = await redis.get('key');
91
+ ```
92
+
93
+ ```python
94
+ # Python — redis-py
95
+ import redis
96
+ import os
97
+
98
+ r = redis.from_url(os.getenv("REDIS_URL", "redis://redis:6379"))
99
+
100
+ r.set("key", "value", ex=3600)
101
+ value = r.get("key")
102
+ ```
103
+
104
+ > **Network rule:** Redis is on `internal` only — it is **never** exposed publicly. RedisInsight (the UI) is on `traefik + internal` so it can be accessed via browser. Your app connects to `redis:6379` on the `internal` network.
105
+
106
+ ---
107
+
108
+ ## 3. 🔄 Job Queues: BullMQ (uses Redis)
109
+
110
+ **BullMQ** is the recommended job queue solution. It uses Redis as its backend — so if Redis is already installed (from item 2), BullMQ is ready to use with no extra infrastructure.
111
+
112
+ **BullMQ is a library, not a separate service.** It runs inside your application code.
113
+
114
+ ### When to use BullMQ
115
+ - Sending emails asynchronously
116
+ - Processing uploaded files or images
117
+ - Scheduling recurring tasks (cron-like)
118
+ - Handling webhooks with retry logic
119
+ - Any long-running task that shouldn't block an HTTP response
120
+
121
+ ### BullMQ Integration (Node.js / TypeScript)
122
+
123
+ ```typescript
124
+ import { Queue, Worker } from 'bullmq';
125
+ import IORedis from 'ioredis';
126
+
127
+ const connection = new IORedis(process.env.REDIS_URL || 'redis://redis:6379', {
128
+ maxRetriesPerRequest: null, // required for BullMQ
129
+ });
130
+
131
+ // --- Producer (add jobs to the queue) ---
132
+ const emailQueue = new Queue('emails', { connection });
133
+
134
+ await emailQueue.add('send-welcome', {
135
+ to: 'user@example.com',
136
+ subject: 'Welcome!',
137
+ });
138
+
139
+ // --- Worker (process jobs from the queue) ---
140
+ const worker = new Worker('emails', async (job) => {
141
+ const { to, subject } = job.data;
142
+ await sendEmail(to, subject); // your email logic
143
+ }, { connection });
144
+
145
+ worker.on('completed', (job) => console.log(`Job ${job.id} completed`));
146
+ worker.on('failed', (job, err) => console.error(`Job ${job?.id} failed:`, err));
147
+ ```
148
+
149
+ ### BullMQ Integration (Python — with rq or celery as alternatives)
150
+
151
+ > BullMQ is Node.js-native. For Python apps, use **Celery + Redis** or **rq + Redis** instead — same Redis instance, same connection string.
152
+
153
+ ```python
154
+ # Python — Celery with Redis broker
155
+ from celery import Celery
156
+
157
+ app = Celery('tasks', broker=os.getenv('REDIS_URL', 'redis://redis:6379/0'))
158
+
159
+ @app.task
160
+ def send_email(to: str, subject: str):
161
+ # your email logic
162
+ pass
163
+
164
+ # Calling the task (async)
165
+ send_email.delay('user@example.com', 'Welcome!')
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Decision Guide
171
+
172
+ | Need | Recommended Solution |
173
+ |---|---|
174
+ | Store and query structured data | NocoDB + PostgreSQL |
175
+ | Cache API responses, store sessions | Redis |
176
+ | Background jobs, async tasks, retries | BullMQ (Node.js) / Celery (Python) — both use Redis |
177
+ | Visual database management | NocoDB UI (built-in) |
178
+ | Visual Redis inspection | RedisInsight |
179
+ | All of the above | Full stack compose above |