@rebasepro/cli 0.4.0 → 0.6.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 (67) hide show
  1. package/README.md +20 -28
  2. package/dist/commands/init.d.ts +3 -0
  3. package/dist/commands/skills.d.ts +1 -0
  4. package/dist/index.cjs +1843 -1414
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.es.js +1620 -1257
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/utils/package-manager.d.ts +2 -0
  9. package/package.json +15 -13
  10. package/skills/rebase-api/SKILL.md +662 -0
  11. package/skills/rebase-api/references/.gitkeep +3 -0
  12. package/skills/rebase-auth/SKILL.md +1143 -0
  13. package/skills/rebase-auth/references/.gitkeep +3 -0
  14. package/skills/rebase-backend-postgres/SKILL.md +633 -0
  15. package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
  16. package/skills/rebase-basics/SKILL.md +749 -0
  17. package/skills/rebase-basics/references/.gitkeep +3 -0
  18. package/skills/rebase-collections/SKILL.md +1328 -0
  19. package/skills/rebase-collections/references/.gitkeep +3 -0
  20. package/skills/rebase-cron-jobs/SKILL.md +699 -0
  21. package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
  22. package/skills/rebase-custom-functions/SKILL.md +233 -0
  23. package/skills/rebase-deployment/SKILL.md +583 -0
  24. package/skills/rebase-deployment/references/.gitkeep +3 -0
  25. package/skills/rebase-design-language/SKILL.md +664 -0
  26. package/skills/rebase-email/SKILL.md +701 -0
  27. package/skills/rebase-email/references/.gitkeep +1 -0
  28. package/skills/rebase-entity-history/SKILL.md +485 -0
  29. package/skills/rebase-entity-history/references/.gitkeep +1 -0
  30. package/skills/rebase-local-env-setup/SKILL.md +189 -0
  31. package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
  32. package/skills/rebase-realtime/SKILL.md +755 -0
  33. package/skills/rebase-realtime/references/.gitkeep +3 -0
  34. package/skills/rebase-sdk/SKILL.md +594 -0
  35. package/skills/rebase-sdk/references/.gitkeep +0 -0
  36. package/skills/rebase-storage/SKILL.md +765 -0
  37. package/skills/rebase-storage/references/.gitkeep +3 -0
  38. package/skills/rebase-studio/SKILL.md +746 -0
  39. package/skills/rebase-studio/references/.gitkeep +3 -0
  40. package/skills/rebase-ui-components/SKILL.md +1411 -0
  41. package/skills/rebase-ui-components/references/.gitkeep +3 -0
  42. package/skills/rebase-webhooks/SKILL.md +623 -0
  43. package/skills/rebase-webhooks/references/.gitkeep +1 -0
  44. package/templates/template/AGENTS.md +2 -0
  45. package/templates/template/CLAUDE.md +2 -0
  46. package/templates/template/ai-instructions.md +6 -3
  47. package/templates/template/backend/drizzle.config.ts +8 -5
  48. package/templates/template/backend/package.json +1 -1
  49. package/templates/template/backend/src/env.ts +1 -1
  50. package/templates/template/backend/src/index.ts +9 -6
  51. package/templates/template/config/collections/posts.ts +1 -1
  52. package/templates/template/config/collections/presets/blank/index.ts +3 -0
  53. package/templates/template/config/collections/presets/ecommerce/categories.ts +38 -0
  54. package/templates/template/config/collections/presets/ecommerce/index.ts +6 -0
  55. package/templates/template/config/collections/presets/ecommerce/orders.ts +64 -0
  56. package/templates/template/config/collections/presets/ecommerce/products.ts +62 -0
  57. package/templates/template/config/collections/users.ts +7 -10
  58. package/templates/template/docker-compose.yml +1 -1
  59. package/templates/template/frontend/package.json +2 -2
  60. package/templates/template/frontend/src/App.tsx +1 -7
  61. package/templates/template/frontend/vite.config.ts +0 -1
  62. package/templates/template/package.json +7 -0
  63. package/dist/commands/cli.test.d.ts +0 -1
  64. package/dist/commands/dev.test.d.ts +0 -1
  65. package/dist/commands/init.test.d.ts +0 -1
  66. package/dist/utils/package-manager.test.d.ts +0 -1
  67. package/dist/utils/project.test.d.ts +0 -1
@@ -0,0 +1,3 @@
1
+ # References
2
+
3
+ Reference documentation for the rebase-collections skill. Add detailed reference files here.
@@ -0,0 +1,699 @@
1
+ ---
2
+ name: rebase-cron-jobs
3
+ description: Guide for scheduling recurring background tasks with Rebase's built-in cron job system. Use this skill when the user needs background jobs, data cleanup, report generation, external API syncing, or any scheduled task.
4
+ ---
5
+
6
+ # Rebase Cron Jobs
7
+
8
+ > **IMPORTANT FOR AGENTS**: Rebase has a **built-in cron scheduler** — do NOT install external libraries (`node-cron`, `agenda`, `bull`) or set up separate worker processes. Drop a TypeScript file in the `crons/` directory.
9
+
10
+ > **IMPORTANT FOR AGENTS**: Every cron handler receives `ctx.client` — a full `RebaseClient` instance. This is **THE** primary way cron jobs read/write data. Always use `ctx.client` in examples, never raw SQL or direct DB imports.
11
+
12
+ ## Overview
13
+
14
+ Rebase includes a built-in cron job scheduler for running recurring background tasks. Cron jobs follow the **file-based discovery** pattern — drop a TypeScript file in `crons/`, and it's automatically registered and scheduled.
15
+
16
+ - **Zero dependencies** — No external scheduler libraries needed
17
+ - **`ctx.client`** — Full `RebaseClient` for CRUD, auth, storage, and more
18
+ - **Concurrency guard** — A job that's still running is skipped, never double-executed
19
+ - **Timeout enforcement** — Handlers are raced against a configurable timeout
20
+ - **Studio dashboard** — Monitor all jobs, view execution history, trigger manually
21
+ - **Admin REST API** — List, trigger, enable/disable, and view logs
22
+ - **Database persistence** — Execution logs stored in `rebase.cron_logs` (PostgreSQL)
23
+
24
+ ## Setup
25
+
26
+ Enable cron jobs by adding `cronsDir` to your backend config:
27
+
28
+ ```typescript
29
+ const backend = await initializeRebaseBackend({
30
+ // ... other config
31
+ cronsDir: path.resolve(__dirname, "../crons"), // ← add this
32
+ });
33
+ ```
34
+
35
+ ### Configuration Options
36
+
37
+ | Option | Type | Default | Description |
38
+ |--------|------|---------|-------------|
39
+ | `cronsDir` | `string` | — | Absolute path to the directory containing cron job files. Required to enable cron jobs. |
40
+ | `cronPersistence` | `boolean` | `true` | Enable/disable database persistence for execution logs. When `false`, logs are kept in-memory only. |
41
+
42
+ ```typescript
43
+ const backend = await initializeRebaseBackend({
44
+ cronsDir: path.resolve(__dirname, "../crons"),
45
+ cronPersistence: false, // disable DB persistence (in-memory only)
46
+ });
47
+ ```
48
+
49
+ ### What Happens on Startup
50
+
51
+ Rebase will:
52
+ 1. Scan the directory for `.ts` / `.js` files
53
+ 2. **Validate** each job's cron schedule — invalid schedules are rejected with an error and the job is NOT registered
54
+ 3. Register each default export as a cron job
55
+ 4. If a **duplicate job ID** is found, the previous job is overwritten with a warning
56
+ 5. Auto-create the `rebase.cron_logs` table in PostgreSQL (unless `cronPersistence: false`)
57
+ 6. Mount admin REST routes at `/api/cron` (protected by `requireAuth` + `requireAdmin`)
58
+ 7. Seed in-memory counters (`totalRuns`, `totalFailures`, `lastRunAt`) from the database
59
+ 8. Start the scheduler
60
+
61
+ ## Defining a Cron Job
62
+
63
+ Create a file in your `backend/crons/` directory that default-exports a `CronJobDefinition`:
64
+
65
+ ```typescript
66
+ // backend/crons/cleanup-sessions.ts
67
+ import type { CronJobDefinition } from "@rebasepro/types";
68
+
69
+ const job: CronJobDefinition = {
70
+ schedule: "0 3 * * *", // daily at 3 AM
71
+ name: "Cleanup Expired Sessions",
72
+ description: "Removes sessions older than 30 days",
73
+ timeoutSeconds: 60, // max execution time (default: 300)
74
+
75
+ async handler(ctx) {
76
+ ctx.log("Starting session cleanup...");
77
+
78
+ // ✅ Use ctx.client to interact with your data
79
+ const { data: oldSessions } = await ctx.client.collection("sessions").getList({
80
+ filter: { createdAt: { lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() } },
81
+ limit: 500,
82
+ });
83
+
84
+ for (const session of oldSessions) {
85
+ await ctx.client.collection("sessions").delete(session.id);
86
+ }
87
+
88
+ ctx.log(`Cleaned up ${oldSessions.length} expired sessions`);
89
+
90
+ // Return value is stored in the log and shown in Studio
91
+ return { deletedSessions: oldSessions.length };
92
+ },
93
+ };
94
+
95
+ export default job;
96
+ ```
97
+
98
+ The **filename** (without extension) becomes the job's unique ID — e.g., `cleanup-sessions`.
99
+
100
+ ### More Examples
101
+
102
+ #### Sync Data from an External API
103
+
104
+ ```typescript
105
+ // backend/crons/sync-exchange-rates.ts
106
+ import type { CronJobDefinition } from "@rebasepro/types";
107
+
108
+ const job: CronJobDefinition = {
109
+ schedule: "0 */6 * * *", // every 6 hours
110
+ name: "Sync Exchange Rates",
111
+ description: "Pulls latest rates from Open Exchange Rates API",
112
+ timeoutSeconds: 30,
113
+
114
+ async handler(ctx) {
115
+ const res = await fetch("https://api.exchangerate.host/latest");
116
+ const data = await res.json();
117
+
118
+ ctx.log("Fetched rates for", Object.keys(data.rates).length, "currencies");
119
+
120
+ // Upsert into your collection using ctx.client
121
+ for (const [currency, rate] of Object.entries(data.rates)) {
122
+ const existing = await ctx.client.collection("exchange_rates").getList({
123
+ filter: { currency },
124
+ limit: 1,
125
+ });
126
+
127
+ if (existing.data.length > 0) {
128
+ await ctx.client.collection("exchange_rates").update(existing.data[0].id, {
129
+ rate: rate as number,
130
+ updatedAt: new Date().toISOString(),
131
+ });
132
+ } else {
133
+ await ctx.client.collection("exchange_rates").create({
134
+ currency,
135
+ rate: rate as number,
136
+ });
137
+ }
138
+ }
139
+
140
+ return { currenciesUpdated: Object.keys(data.rates).length };
141
+ },
142
+ };
143
+
144
+ export default job;
145
+ ```
146
+
147
+ #### Send Daily Digest Emails
148
+
149
+ ```typescript
150
+ // backend/crons/daily-digest.ts
151
+ import type { CronJobDefinition } from "@rebasepro/types";
152
+
153
+ const job: CronJobDefinition = {
154
+ schedule: "0 8 * * 1-5", // 8 AM weekdays
155
+ name: "Daily Digest",
156
+ description: "Sends a summary email to all users with unread notifications",
157
+ timeoutSeconds: 120,
158
+
159
+ async handler(ctx) {
160
+ const { data: users } = await ctx.client.collection("users").getList({
161
+ filter: { digestEnabled: true },
162
+ });
163
+
164
+ let sent = 0;
165
+ for (const user of users) {
166
+ const { data: notifications } = await ctx.client.collection("notifications").getList({
167
+ filter: { userId: user.id, read: false },
168
+ });
169
+
170
+ if (notifications.length > 0) {
171
+ ctx.log(`Sending digest to ${user.email} (${notifications.length} unread)`);
172
+ // Send email via your preferred method
173
+ sent++;
174
+ }
175
+ }
176
+
177
+ return { emailsSent: sent, usersChecked: users.length };
178
+ },
179
+ };
180
+
181
+ export default job;
182
+ ```
183
+
184
+ ## CronJobDefinition Interface
185
+
186
+ ```typescript
187
+ interface CronJobDefinition {
188
+ schedule: string; // Cron expression (5-field)
189
+ name: string; // Human-readable name for Studio
190
+ description?: string; // Optional description
191
+ enabled?: boolean; // Start enabled? (default: true)
192
+ timeoutSeconds?: number; // Max execution time (default: 300)
193
+ handler: (ctx: CronJobContext) => Promise<unknown> | unknown;
194
+ }
195
+ ```
196
+
197
+ | Property | Type | Default | Description |
198
+ |----------|------|---------|-------------|
199
+ | `schedule` | `string` | — | Standard 5-field cron expression. **Validated on registration** — invalid expressions are rejected. |
200
+ | `name` | `string` | — | Human-readable name shown in Studio UI and API responses. |
201
+ | `description` | `string?` | `undefined` | Optional description shown in Studio. |
202
+ | `enabled` | `boolean?` | `true` | Whether the job starts enabled. Can be toggled at runtime via the Admin API. |
203
+ | `timeoutSeconds` | `number?` | `300` | Maximum seconds the handler may run before being timed out. |
204
+ | `handler` | `(ctx) => Promise<unknown> \| unknown` | — | The function executed on each tick. May return JSON-serialisable data stored in the log. |
205
+
206
+ ## CronJobContext (Handler Argument)
207
+
208
+ > **IMPORTANT FOR AGENTS**: `ctx.client` is a full `RebaseClient` instance — the **same admin-level server client** used by the Rebase singleton. It bypasses the network and goes through Hono's internal request handler. Use it for all data operations inside cron handlers.
209
+
210
+ ```typescript
211
+ interface CronJobContext {
212
+ /** The job's unique ID (derived from filename). */
213
+ jobId: string;
214
+
215
+ /** The current scheduled tick timestamp. */
216
+ scheduledAt: Date;
217
+
218
+ /** A simple logger scoped to this job run — output captured in the execution log. */
219
+ log: (...args: unknown[]) => void;
220
+
221
+ /** The RebaseClient instance to interact with the database. */
222
+ client: RebaseClient;
223
+ }
224
+ ```
225
+
226
+ | Property | Type | Description |
227
+ |----------|------|-------------|
228
+ | `jobId` | `string` | Derived from the filename (e.g. `cleanup-sessions`). |
229
+ | `scheduledAt` | `Date` | The timestamp when this execution was scheduled to start. |
230
+ | `log` | `(...args: unknown[]) => void` | Logger whose output is captured in `CronJobLogEntry.logs`. Use like `console.log`. |
231
+ | `client` | `RebaseClient` | Admin-level client for CRUD, auth, storage, and any SDK operation. |
232
+
233
+ ### Using `ctx.client`
234
+
235
+ `ctx.client` is the same `RebaseClient` returned by `createRebaseClient()`. It supports all SDK operations:
236
+
237
+ ```typescript
238
+ async handler(ctx) {
239
+ // Collection CRUD
240
+ const { data } = await ctx.client.collection("orders").getList({ limit: 100 });
241
+ await ctx.client.collection("orders").update(id, { status: "archived" });
242
+ await ctx.client.collection("orders").delete(id);
243
+ await ctx.client.collection("metrics").create({ key: "daily_total", value: 42 });
244
+
245
+ // Filtering and sorting
246
+ const { data: stale } = await ctx.client.collection("tokens").getList({
247
+ filter: { expiresAt: { lt: new Date().toISOString() } },
248
+ sort: "-createdAt",
249
+ limit: 500,
250
+ });
251
+
252
+ // Logging
253
+ ctx.log("Processed", data.length, "records");
254
+ }
255
+ ```
256
+
257
+ ## Schedule Syntax (Cron Expressions)
258
+
259
+ Standard 5-field cron format: `minute hour day-of-month month day-of-week`
260
+
261
+ | Expression | Meaning |
262
+ |------------|---------|
263
+ | `* * * * *` | Every minute |
264
+ | `*/5 * * * *` | Every 5 minutes |
265
+ | `0 * * * *` | Every hour |
266
+ | `0 3 * * *` | Daily at 3:00 AM |
267
+ | `0 0 * * 1` | Every Monday at midnight |
268
+ | `0 9 1 * *` | First of each month at 9 AM |
269
+ | `0 9-17 * * 1-5` | Hourly, 9 AM–5 PM, weekdays |
270
+
271
+ ### Supported Field Syntax
272
+
273
+ | Syntax | Example | Meaning |
274
+ |--------|---------|---------|
275
+ | `*` | `* * * * *` | Every value in the field's range |
276
+ | `N` | `30 * * * *` | Exact value |
277
+ | `N-M` | `9-17` | Range (inclusive) |
278
+ | `*/S` | `*/5` | Every S values starting from the field minimum |
279
+ | `N/S` | `10/15` | Every S values starting from N |
280
+ | `N-M/S` | `0-30/5` | Every S values within range N–M |
281
+ | `A,B,C` | `0,15,30,45` | Comma-separated list (combinable with above) |
282
+
283
+ ### Schedule Validation
284
+
285
+ Schedules are **validated on registration**. Invalid expressions are rejected with a warning log and the job is **not** registered. Validation checks:
286
+
287
+ - Exactly 5 fields (minute, hour, day-of-month, month, day-of-week)
288
+ - All values within valid ranges: minute `0–59`, hour `0–23`, dom `1–31`, month `1–12`, dow `0–6`
289
+ - No unparseable tokens
290
+
291
+ ### Minimum Schedule Interval
292
+
293
+ A **5-second minimum delay** is enforced between scheduled executions. Even if a cron expression would produce a sub-5-second gap (due to event loop jitter or clock drift), the scheduler clamps the delay to at least 5000ms. This prevents tight re-execution loops.
294
+
295
+ ## Concurrency Guard
296
+
297
+ > **WARNING FOR AGENTS**: A cron job **never** runs two instances concurrently. If a scheduled tick fires while the previous run is still executing, it is **skipped** (not queued). The scheduler re-schedules for the next tick.
298
+
299
+ This also applies to **manual triggers** via `POST /api/cron/:id/trigger`. If the job is currently executing, the API returns immediately with a log entry containing:
300
+
301
+ ```json
302
+ {
303
+ "log": {
304
+ "jobId": "my-job",
305
+ "durationMs": 0,
306
+ "success": true,
307
+ "result": { "skipped": true, "reason": "already_executing" },
308
+ "logs": ["Skipped: job is already running"],
309
+ "manual": true
310
+ }
311
+ }
312
+ ```
313
+
314
+ ## Timeout Behavior
315
+
316
+ Each handler invocation is **raced** against a timeout:
317
+
318
+ ```typescript
319
+ const timeout = (job.timeoutSeconds ?? 300) * 1000;
320
+ const result = await Promise.race([handlerPromise, timeoutPromise]);
321
+ ```
322
+
323
+ - Default timeout: **300 seconds** (5 minutes)
324
+ - If the timeout fires first, the job is marked as **failed** with error `Cron job "<id>" timed out after <N>ms`
325
+ - The timeout timer is always cleaned up via `clearTimeout`, even on success or error
326
+ - The `executing` flag is always cleared in a `finally` block, so the concurrency guard recovers correctly
327
+
328
+ ## Persistence
329
+
330
+ ### Database Persistence (`rebase.cron_logs`)
331
+
332
+ When `cronPersistence` is `true` (default) and the data driver supports SQL, Rebase automatically creates and uses a `rebase.cron_logs` table:
333
+
334
+ ```sql
335
+ CREATE TABLE rebase.cron_logs (
336
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
337
+ job_id TEXT NOT NULL,
338
+ started_at TIMESTAMPTZ NOT NULL,
339
+ finished_at TIMESTAMPTZ NOT NULL,
340
+ duration_ms INTEGER NOT NULL,
341
+ success BOOLEAN NOT NULL DEFAULT true,
342
+ error TEXT,
343
+ result JSONB,
344
+ logs JSONB,
345
+ manual BOOLEAN NOT NULL DEFAULT false
346
+ );
347
+
348
+ -- Index for efficient per-job log queries
349
+ CREATE INDEX idx_cron_logs_job ON rebase.cron_logs(job_id, started_at DESC);
350
+ ```
351
+
352
+ Log persistence is **non-blocking** — if the database insert fails, the error is logged but the scheduler continues normally.
353
+
354
+ ### In-Memory Ring Buffer
355
+
356
+ Regardless of database persistence, the scheduler keeps an **in-memory ring buffer** of the last **50 log entries per job**. The REST API falls back to this buffer when database persistence is unavailable.
357
+
358
+ ### Fallback Behavior
359
+
360
+ | Driver | Persistence | Behavior |
361
+ |--------|-------------|----------|
362
+ | PostgreSQL | ✅ DB + memory | Logs written to `rebase.cron_logs` and in-memory buffer |
363
+ | PostgreSQL (`cronPersistence: false`) | ❌ Memory only | Logs kept in-memory ring buffer only |
364
+ | Non-SQL (e.g. MongoDB) | ❌ Memory only | Warning logged; no DB persistence |
365
+
366
+ ### Startup Seeding
367
+
368
+ On startup, the scheduler seeds `totalRuns`, `totalFailures`, and `lastRunAt` counters from the database. This is **non-blocking** — the scheduler starts immediately and counters are updated asynchronously.
369
+
370
+ ## REST API
371
+
372
+ All routes are mounted at `/api/cron` and **require admin authentication** (`requireAuth` + `requireAdmin` middleware).
373
+
374
+ > **IMPORTANT FOR AGENTS**: All cron REST endpoints require an admin JWT or service key in the `Authorization` header. Unauthenticated requests will receive 401/403.
375
+
376
+ ### List All Jobs
377
+
378
+ ```
379
+ GET /api/cron
380
+ ```
381
+
382
+ **Response:** `200 OK`
383
+
384
+ ```json
385
+ {
386
+ "jobs": [
387
+ {
388
+ "id": "cleanup-sessions",
389
+ "name": "Cleanup Expired Sessions",
390
+ "description": "Removes sessions older than 30 days",
391
+ "schedule": "0 3 * * *",
392
+ "enabled": true,
393
+ "state": "idle",
394
+ "lastRunAt": "2025-01-15T03:00:00.000Z",
395
+ "nextRunAt": "2025-01-16T03:00:00.000Z",
396
+ "lastDurationMs": 1234,
397
+ "totalRuns": 42,
398
+ "totalFailures": 1
399
+ }
400
+ ]
401
+ }
402
+ ```
403
+
404
+ ### Get Single Job
405
+
406
+ ```
407
+ GET /api/cron/:id
408
+ ```
409
+
410
+ **Response:** `200 OK`
411
+
412
+ ```json
413
+ {
414
+ "job": {
415
+ "id": "cleanup-sessions",
416
+ "name": "Cleanup Expired Sessions",
417
+ "description": "Removes sessions older than 30 days",
418
+ "schedule": "0 3 * * *",
419
+ "enabled": true,
420
+ "state": "idle",
421
+ "lastRunAt": "2025-01-15T03:00:00.000Z",
422
+ "nextRunAt": "2025-01-16T03:00:00.000Z",
423
+ "lastDurationMs": 1234,
424
+ "totalRuns": 42,
425
+ "totalFailures": 1
426
+ }
427
+ }
428
+ ```
429
+
430
+ **Error:** `404 Not Found`
431
+
432
+ ```json
433
+ {
434
+ "error": { "message": "Cron job \"unknown-id\" not found", "code": "NOT_FOUND" }
435
+ }
436
+ ```
437
+
438
+ ### Trigger Job Manually
439
+
440
+ ```
441
+ POST /api/cron/:id/trigger
442
+ ```
443
+
444
+ **Response:** `200 OK`
445
+
446
+ ```json
447
+ {
448
+ "log": {
449
+ "jobId": "cleanup-sessions",
450
+ "startedAt": "2025-01-15T12:00:00.000Z",
451
+ "finishedAt": "2025-01-15T12:00:01.234Z",
452
+ "durationMs": 1234,
453
+ "success": true,
454
+ "result": { "deletedSessions": 15 },
455
+ "logs": ["Starting session cleanup...", "Cleaned up 15 expired sessions"],
456
+ "manual": true
457
+ },
458
+ "job": { /* updated CronJobStatus */ }
459
+ }
460
+ ```
461
+
462
+ > If the job is already executing, the response returns immediately with `result: { skipped: true, reason: "already_executing" }`.
463
+
464
+ ### Get Job Logs
465
+
466
+ ```
467
+ GET /api/cron/:id/logs?limit=10
468
+ ```
469
+
470
+ | Query Param | Type | Default | Description |
471
+ |-------------|------|---------|-------------|
472
+ | `limit` | `number?` | `50` | Maximum number of log entries to return |
473
+
474
+ **Response:** `200 OK`
475
+
476
+ ```json
477
+ {
478
+ "logs": [
479
+ {
480
+ "jobId": "cleanup-sessions",
481
+ "startedAt": "2025-01-15T03:00:00.000Z",
482
+ "finishedAt": "2025-01-15T03:00:01.234Z",
483
+ "durationMs": 1234,
484
+ "success": true,
485
+ "result": { "deletedSessions": 15 },
486
+ "logs": ["Starting session cleanup...", "Cleaned up 15 expired sessions"],
487
+ "manual": false
488
+ }
489
+ ]
490
+ }
491
+ ```
492
+
493
+ Logs are returned **newest first**. When a database store is available, logs are fetched from the database; otherwise, the in-memory ring buffer is used as fallback.
494
+
495
+ ### Enable/Disable a Job
496
+
497
+ ```
498
+ PUT /api/cron/:id
499
+ Content-Type: application/json
500
+
501
+ { "enabled": false }
502
+ ```
503
+
504
+ **Response:** `200 OK`
505
+
506
+ ```json
507
+ {
508
+ "job": {
509
+ "id": "cleanup-sessions",
510
+ "enabled": false,
511
+ "state": "disabled",
512
+ /* ... rest of CronJobStatus */
513
+ }
514
+ }
515
+ ```
516
+
517
+ **Error:** `400 Bad Request` (missing `enabled` field)
518
+
519
+ ```json
520
+ {
521
+ "error": { "message": "Missing 'enabled' boolean in body", "code": "BAD_REQUEST" }
522
+ }
523
+ ```
524
+
525
+ ### API Summary Table
526
+
527
+ | Method | Path | Request Body | Response Shape |
528
+ |--------|------|-------------|----------------|
529
+ | `GET` | `/api/cron` | — | `{ jobs: CronJobStatus[] }` |
530
+ | `GET` | `/api/cron/:id` | — | `{ job: CronJobStatus }` |
531
+ | `POST` | `/api/cron/:id/trigger` | — | `{ log: CronJobLogEntry, job: CronJobStatus }` |
532
+ | `GET` | `/api/cron/:id/logs` | — | `{ logs: CronJobLogEntry[] }` |
533
+ | `PUT` | `/api/cron/:id` | `{ enabled: boolean }` | `{ job: CronJobStatus }` |
534
+
535
+ ## Type Reference
536
+
537
+ ### `CronJobStatus`
538
+
539
+ Returned by all status endpoints. Represents the full runtime state of a registered cron job.
540
+
541
+ ```typescript
542
+ type CronJobRunState = "idle" | "running" | "success" | "error" | "disabled";
543
+
544
+ interface CronJobStatus {
545
+ id: string; // Unique ID from filename
546
+ name: string; // Human-readable name
547
+ description?: string; // Optional description
548
+ schedule: string; // Cron expression
549
+ enabled: boolean; // Currently enabled?
550
+ state: CronJobRunState; // Current run state
551
+ lastRunAt?: string; // ISO timestamp of last execution start
552
+ nextRunAt?: string; // ISO timestamp of next scheduled execution
553
+ lastDurationMs?: number; // Duration of last run in ms
554
+ lastError?: string; // Error message from last failed run
555
+ totalRuns: number; // Total executions since server start (seeded from DB)
556
+ totalFailures: number; // Total failures since server start (seeded from DB)
557
+ }
558
+ ```
559
+
560
+ | Field | Type | Description |
561
+ |-------|------|-------------|
562
+ | `id` | `string` | Derived from filename, e.g. `cleanup-sessions`. |
563
+ | `name` | `string` | Human-readable name from the definition. |
564
+ | `description` | `string?` | Description from the definition. |
565
+ | `schedule` | `string` | The cron schedule expression. |
566
+ | `enabled` | `boolean` | Whether the job is currently enabled. |
567
+ | `state` | `CronJobRunState` | One of: `idle`, `running`, `success`, `error`, `disabled`. |
568
+ | `lastRunAt` | `string?` | ISO timestamp of the last execution start. |
569
+ | `nextRunAt` | `string?` | ISO timestamp of the next scheduled execution. |
570
+ | `lastDurationMs` | `number?` | Duration of the last run in milliseconds. |
571
+ | `lastError` | `string?` | Error message from the last failed run. |
572
+ | `totalRuns` | `number` | Total number of executions (seeded from DB on startup). |
573
+ | `totalFailures` | `number` | Total number of failed executions (seeded from DB on startup). |
574
+
575
+ ### `CronJobRunState`
576
+
577
+ ```typescript
578
+ type CronJobRunState = "idle" | "running" | "success" | "error" | "disabled";
579
+ ```
580
+
581
+ | State | Meaning |
582
+ |-------|---------|
583
+ | `idle` | Enabled and waiting for next scheduled tick |
584
+ | `running` | Handler is currently executing |
585
+ | `success` | Last run completed successfully (transitional — becomes `idle`) |
586
+ | `error` | Last run failed (stays until next successful run) |
587
+ | `disabled` | Job is disabled via API or `enabled: false` in definition |
588
+
589
+ ### `CronJobLogEntry`
590
+
591
+ A single execution log entry, stored in DB and/or in-memory ring buffer.
592
+
593
+ ```typescript
594
+ interface CronJobLogEntry {
595
+ jobId: string; // The job ID this log belongs to
596
+ startedAt: string; // ISO timestamp when execution started
597
+ finishedAt: string; // ISO timestamp when execution finished
598
+ durationMs: number; // Duration in milliseconds
599
+ success: boolean; // Whether this run succeeded
600
+ error?: string; // Error message if the run failed
601
+ result?: unknown; // Arbitrary data returned by the handler
602
+ logs: string[]; // Captured log lines from ctx.log()
603
+ manual?: boolean; // Whether this was a manual trigger
604
+ }
605
+ ```
606
+
607
+ | Field | Type | Description |
608
+ |-------|------|-------------|
609
+ | `jobId` | `string` | The job ID this log belongs to. |
610
+ | `startedAt` | `string` | ISO timestamp when execution started. |
611
+ | `finishedAt` | `string` | ISO timestamp when execution finished. |
612
+ | `durationMs` | `number` | Duration in milliseconds. |
613
+ | `success` | `boolean` | Whether this run succeeded. |
614
+ | `error` | `string?` | Error message if the run failed (timeout, exception, etc.). |
615
+ | `result` | `unknown?` | JSON-serialisable data returned by the handler. |
616
+ | `logs` | `string[]` | Captured log lines from `ctx.log()`. |
617
+ | `manual` | `boolean?` | `true` if triggered via `POST /api/cron/:id/trigger`. |
618
+
619
+ ## Client SDK
620
+
621
+ The Rebase client SDK provides typed methods for all cron operations. All methods require admin authentication.
622
+
623
+ ```typescript
624
+ import { createRebaseClient } from "@rebasepro/client";
625
+
626
+ const client = createRebaseClient({
627
+ baseUrl: "http://localhost:3001",
628
+ token: "your-admin-jwt-or-service-key",
629
+ });
630
+ ```
631
+
632
+ ### SDK Methods
633
+
634
+ | Method | Return Type | Description |
635
+ |--------|------------|-------------|
636
+ | `client.cron.listJobs()` | `Promise<{ jobs: CronJobStatus[] }>` | List all registered cron jobs |
637
+ | `client.cron.getJob(jobId)` | `Promise<{ job: CronJobStatus }>` | Get a single job's status |
638
+ | `client.cron.triggerJob(jobId)` | `Promise<{ log: CronJobLogEntry, job: CronJobStatus }>` | Manually trigger a job |
639
+ | `client.cron.getJobLogs(jobId, options?)` | `Promise<{ logs: CronJobLogEntry[] }>` | Get execution history |
640
+ | `client.cron.toggleJob(jobId, enabled)` | `Promise<{ job: CronJobStatus }>` | Enable or disable a job |
641
+
642
+ ### Usage Examples
643
+
644
+ ```typescript
645
+ // List all jobs
646
+ const { jobs } = await client.cron.listJobs();
647
+ console.log(jobs.map(j => `${j.id}: ${j.state}`));
648
+
649
+ // Get a specific job
650
+ const { job } = await client.cron.getJob("cleanup-sessions");
651
+ console.log(`Next run: ${job.nextRunAt}`);
652
+
653
+ // Trigger a job manually
654
+ const { log, job: updatedJob } = await client.cron.triggerJob("cleanup-sessions");
655
+ if (log.result?.skipped) {
656
+ console.log("Job was already running — skipped");
657
+ } else {
658
+ console.log(`Completed in ${log.durationMs}ms`, log.result);
659
+ }
660
+
661
+ // Get execution history (last 10 entries)
662
+ const { logs } = await client.cron.getJobLogs("cleanup-sessions", { limit: 10 });
663
+
664
+ // Pause a job
665
+ await client.cron.toggleJob("cleanup-sessions", false);
666
+
667
+ // Resume a job
668
+ await client.cron.toggleJob("cleanup-sessions", true);
669
+ ```
670
+
671
+ ## Edge Cases and Gotchas
672
+
673
+ | Scenario | Behavior |
674
+ |----------|----------|
675
+ | **Invalid cron expression** | Job is rejected at registration with a warning log. Not registered. |
676
+ | **Duplicate job ID** (same filename in crons dir) | Previous job is stopped and overwritten with a warning log. |
677
+ | **Job still running when next tick fires** | Scheduled tick is skipped; scheduler re-schedules for the next tick. |
678
+ | **Manual trigger while running** | Returns immediately with `{ skipped: true, reason: "already_executing" }`. |
679
+ | **Handler throws an exception** | Marked as failed; `error` field populated; `totalFailures` incremented; `executing` flag cleared. |
680
+ | **Handler exceeds timeout** | `Promise.race` rejects with timeout error; marked as failed. Timeout timer is always cleaned up. |
681
+ | **Scheduler stopped while handler runs** | Handler runs to completion (it's async), but no further scheduling occurs. |
682
+ | **Non-SQL driver (e.g. MongoDB)** | Warning logged; cron jobs run normally but logs are not persisted to DB (in-memory only). |
683
+ | **DB persistence insert fails** | Error logged; scheduler continues normally (non-blocking). |
684
+ | **`cronPersistence: false`** | No `rebase.cron_logs` table created; logs kept in-memory ring buffer (50 per job). |
685
+ | **Job registered after scheduler started** | Automatically scheduled — late-registered jobs don't sit idle. |
686
+
687
+ ## Common Use Cases
688
+
689
+ - **Data cleanup** — Remove expired sessions, stale tokens, orphaned records
690
+ - **Report generation** — Build daily/weekly summaries, export analytics
691
+ - **External API sync** — Pull data from third-party APIs on a schedule
692
+ - **Health checks** — Monitor uptime, memory usage, database connectivity
693
+ - **Notification batching** — Send digest emails, aggregate alerts
694
+ - **Cache warming** — Pre-compute expensive queries during off-peak hours
695
+
696
+ ## References
697
+
698
+ - **Documentation:** [rebase.pro/docs/backend/cron-jobs](https://rebase.pro/docs/backend/cron-jobs)
699
+ - **GitHub:** [github.com/rebasepro/rebase](https://github.com/rebasepro/rebase)