Skip to content

Commit 971c6ee

Browse files
authored
Merge pull request #100 from techdiary-dev/copilot/migrate-article-cleanup-cron
Migrate article cleanup cron from Cloudflare Worker to Inngest
2 parents 7b32e82 + 972dcce commit 971c6ee

9 files changed

Lines changed: 47 additions & 205 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,11 @@ Cloudflare R2 (S3-compatible):
141141
- `S3_BUCKET` - R2 bucket name
142142
- `S3_ACCESS_KEY_ID` - R2 access key
143143
- `S3_SECRET_ACCESS_KEY` - R2 secret key
144-
- `CRON_SECRET` - Shared secret for `x-cron-secret` header on cron API endpoint
144+
145+
Inngest:
146+
147+
- `INNGEST_EVENT_KEY` - Inngest event key (optional for local dev)
148+
- `INNGEST_SIGNING_KEY` - Inngest signing key (optional for local dev)
145149

146150
## Key Features Implementation
147151

@@ -361,8 +365,8 @@ const feedQuery = useInfiniteQuery({
361365
- **Bengali Language Support**: Custom font loading (Kohinoor Bangla) and i18n in `src/i18n/`
362366
- **SEO Optimization**: Dynamic sitemaps in `src/app/sitemaps/`, Open Graph tags, and schema markup
363367
- **No test framework**: There are no automated tests — use `bun run play` for backend experimentation
364-
- **Cloudflare Workers**: `wrangler.toml` configures a separate cron worker (`src/workers/cron-worker.ts`). `bun run wrangler:dev` starts it locally. The worker fires on `0 2 * * *` and calls `POST /api/cron/cleanup-articles` with `x-cron-secret` header.
365-
- **Article soft-delete**: Articles have a `delete_scheduled_at` field. Setting it schedules permanent deletion; `article-cleanup-service.ts` processes them when the cron fires. Use `restoreScheduleDeletedArticle` to cancel.
368+
- **Cloudflare Workers**: `wrangler.toml` still references `src/workers/cron-worker.ts` but the cron trigger has been removed. Article cleanup is now handled by Inngest (see below).
369+
- **Article soft-delete**: Articles have a `delete_scheduled_at` field. Setting it schedules permanent deletion; `article-cleanup-service.ts` processes them when the **Inngest cron** (`cleanup-expired-articles`, `0 2 * * *` UTC) fires. Use `restoreScheduleDeletedArticle` to cancel.
366370

367371
## Caching & ISR (Incremental Static Regeneration)
368372

CLOUDFLARE_CRON_SETUP.md

Lines changed: 16 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,28 @@
1-
# Cloudflare Cron Setup for Article Cleanup
1+
# Article Cleanup Cron Setup
22

3-
This document explains how to set up Cloudflare Cron Triggers to automatically delete expired articles.
3+
> **Note**: The Cloudflare Worker cron for article cleanup has been retired.
4+
> Article cleanup is now handled by an **Inngest cron function** that runs daily at 2:00 AM UTC.
45
5-
## Overview
6+
## Current Setup
67

7-
The system consists of:
8-
1. **API Route**: `/api/cron/cleanup-articles` - Handles the actual cleanup logic
9-
2. **Cloudflare Worker**: `src/workers/cron-worker.ts` - Cron trigger that calls the API
10-
3. **Backend Service**: `src/backend/services/article-cleanup-service.ts` - Database operations
8+
Article cleanup runs as an Inngest cron function registered at `/api/inngest`.
119

12-
## Setup Instructions
10+
- **Function ID**: `cleanup-expired-articles`
11+
- **Schedule**: `0 2 * * *` (daily at 2:00 AM UTC)
12+
- **Source**: `src/lib/inngest.ts``cleanupExpiredArticlesFn`
13+
- **Logic**: `src/backend/services/article-cleanup-service.ts``deleteExpiredArticles()`
1314

14-
### 1. Install Wrangler CLI
15+
## Manual Runs
1516

16-
```bash
17-
npm install -g wrangler
18-
# or
19-
bun install -g wrangler
20-
```
21-
22-
### 2. Authenticate with Cloudflare
23-
24-
```bash
25-
wrangler login
26-
```
27-
28-
### 3. Configure Environment Variables
29-
30-
Update `wrangler.toml` with your actual domain:
31-
32-
```toml
33-
[vars]
34-
CRON_TARGET_URL = "https://your-actual-domain.com/api/cron/cleanup-articles"
35-
```
36-
37-
### 4. Set Secret (Optional but Recommended)
38-
39-
For additional security, set a secret that the cron worker will send:
40-
41-
```bash
42-
wrangler secret put CRON_SECRET
43-
# Enter your secret when prompted
44-
```
45-
46-
Then add the same secret to your Next.js environment:
47-
48-
```bash
49-
# .env.local
50-
CRON_SECRET=your-secret-key
51-
```
52-
53-
### 5. Deploy the Cron Worker
54-
55-
```bash
56-
wrangler deploy src/workers/cron-worker.ts
57-
```
58-
59-
### 6. Verify Cron Schedule
60-
61-
The cron is configured to run daily at 2:00 AM UTC. You can modify the schedule in `wrangler.toml`:
62-
63-
```toml
64-
[triggers]
65-
crons = [
66-
"0 2 * * *" # Daily at 2:00 AM UTC
67-
]
68-
```
69-
70-
Other common schedules:
71-
- `"0 * * * *"` - Every hour
72-
- `"0 2 * * 0"` - Every Sunday at 2:00 AM
73-
- `"0 2 1 * *"` - First day of every month at 2:00 AM
74-
75-
### 7. Monitor Cron Executions
76-
77-
You can monitor executions in the Cloudflare dashboard:
78-
1. Go to Workers & Pages
79-
2. Select your worker
80-
3. Check the "Logs" tab for execution logs
81-
82-
## Manual Testing
83-
84-
You can test the cleanup manually by calling the API directly:
85-
86-
```bash
87-
# GET request for testing
88-
curl https://your-domain.com/api/cron/cleanup-articles
89-
90-
# POST request (mimics cron call)
91-
curl -X POST https://your-domain.com/api/cron/cleanup-articles \
92-
-H "Content-Type: application/json" \
93-
-H "x-cron-secret: your-secret-key"
94-
```
17+
Trigger a manual run from the [Inngest dashboard](https://app.inngest.com) by invoking the `cleanup-expired-articles` function.
9518

9619
## How It Works
9720

9821
1. **Article Scheduling**: Articles can be scheduled for deletion by setting the `delete_scheduled_at` field
99-
2. **Cron Trigger**: Cloudflare runs the worker daily at 2:00 AM UTC
100-
3. **API Call**: Worker calls `/api/cron/cleanup-articles` endpoint
101-
4. **Cleanup Logic**: Service finds articles where `delete_scheduled_at < current_time` and deletes them
102-
5. **Response**: API returns count of deleted articles and their details
103-
104-
## Database Schema
105-
106-
Articles are scheduled for deletion using the `delete_scheduled_at` timestamp field:
107-
108-
```sql
109-
-- Example of scheduling an article for deletion in 30 days
110-
UPDATE articles
111-
SET delete_scheduled_at = NOW() + INTERVAL '30 days'
112-
WHERE id = 'article-id';
113-
```
114-
115-
## Additional Functions
116-
117-
The cleanup service also provides:
118-
- `scheduleArticleForDeletion(articleId, deleteAt)` - Schedule an article
119-
- `cancelScheduledDeletion(articleId)` - Cancel scheduled deletion
120-
- `getScheduledArticles()` - Get all articles scheduled for deletion
121-
122-
## Troubleshooting
123-
124-
### Common Issues
125-
126-
1. **404 Error**: Check that your `CRON_TARGET_URL` is correct
127-
2. **401 Unauthorized**: Verify `CRON_SECRET` matches between worker and API
128-
3. **500 Error**: Check database connection and permissions
129-
4. **No Articles Deleted**: Verify articles have `delete_scheduled_at` set and are past due
130-
131-
### Debugging
132-
133-
1. Check Cloudflare Worker logs in the dashboard
134-
2. Check your Next.js application logs
135-
3. Test the API endpoint manually
136-
4. Verify database records with `getScheduledArticles()`
22+
2. **Inngest Cron**: Inngest triggers the function daily at 2:00 AM UTC
23+
3. **Cleanup Logic**: `deleteExpiredArticles()` finds articles where `delete_scheduled_at < current_time` and deletes them from the database and MeiliSearch
13724

138-
## Security Considerations
25+
## Required Environment Variables
13926

140-
1. Use `CRON_SECRET` to verify requests come from your worker
141-
2. Consider rate limiting the endpoint
142-
3. Log all cleanup operations for audit trails
143-
4. Ensure proper database permissions for the cleanup operations
27+
- `INNGEST_EVENT_KEY` — Inngest event key (optional for local dev)
28+
- `INNGEST_SIGNING_KEY` — Inngest signing key (optional for local dev)

src/app/api/cron/cleanup-articles/route.ts

Lines changed: 0 additions & 52 deletions
This file was deleted.

src/app/api/inngest/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { serve } from "inngest/next";
2-
import { inngest, persistNotificationFn } from "@/lib/inngest";
2+
import {
3+
inngest,
4+
persistNotificationFn,
5+
cleanupExpiredArticlesFn,
6+
} from "@/lib/inngest";
37

48
export const { GET, POST, PUT } = serve({
59
client: inngest,
6-
functions: [persistNotificationFn],
10+
functions: [persistNotificationFn, cleanupExpiredArticlesFn],
711
});

src/backend/services/article-cleanup-service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use server";
2-
import { and, lt, neq } from "sqlkit";
2+
import { and, lt, lte, neq } from "sqlkit";
33
import { persistenceRepository } from "../persistence/persistence-repositories";
44
import { handleActionException } from "./RepositoryException";
55
import { deleteArticleById } from "./search.service";
@@ -14,7 +14,7 @@ export async function deleteExpiredArticles() {
1414
const articlesToDelete = await persistenceRepository.article.find({
1515
where: and(
1616
neq("delete_scheduled_at", null),
17-
lt("delete_scheduled_at", currentTime)
17+
lte("delete_scheduled_at", currentTime),
1818
),
1919
});
2020

@@ -25,12 +25,12 @@ export async function deleteExpiredArticles() {
2525
const deleteResult = await persistenceRepository.article.delete({
2626
where: and(
2727
neq("delete_scheduled_at", null),
28-
lt("delete_scheduled_at", currentTime)
28+
lte("delete_scheduled_at", currentTime),
2929
),
3030
});
3131

3232
console.log(
33-
`Successfully deleted ${deleteResult?.rowCount} expired articles`
33+
`Successfully deleted ${deleteResult?.rowCount} expired articles`,
3434
);
3535

3636
return {

src/backend/services/search.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const syncArticleById = async (articleId: string) => {
7171

7272
if (!article) {
7373
throw new Error(
74-
`Article with ID ${articleId} not found or not published.`
74+
`Article with ID ${articleId} not found or not published.`,
7575
);
7676
}
7777

@@ -91,6 +91,7 @@ export const syncArticleById = async (articleId: string) => {
9191
console.error(`Error syncing article ${articleId}:`, error);
9292
}
9393
};
94+
9495
export const deleteArticleById = async (articleId: string) => {
9596
try {
9697
const response = await index.deleteDocument(articleId);

src/env.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ export const env = createEnv({
2020
S3_ACCESS_SECRET: z.string().min(1),
2121
S3_BUCKET: z.string().min(1),
2222

23-
CRON_SECRET: z.string().optional(),
24-
2523
// Inngest
2624
INNGEST_EVENT_KEY: z.string().optional(),
2725
INNGEST_SIGNING_KEY: z.string().optional(),
@@ -50,8 +48,6 @@ export const env = createEnv({
5048
S3_ACCESS_SECRET: process.env.S3_ACCESS_SECRET,
5149
S3_BUCKET: process.env.S3_BUCKET,
5250

53-
CRON_SECRET: process.env.CRON_SECRET,
54-
5551
INNGEST_EVENT_KEY: process.env.INNGEST_EVENT_KEY,
5652
INNGEST_SIGNING_KEY: process.env.INNGEST_SIGNING_KEY,
5753
},

src/lib/inngest.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { persistenceRepository } from "@/backend/persistence/persistence-repositories";
88
import { ActionException } from "@/backend/services/RepositoryException";
99
import { buildPersistableNotification } from "@/backend/services/notifications.payload";
10+
import { deleteExpiredArticles } from "@/backend/services/article-cleanup-service";
1011

1112
const notificationPayloadSchema = z.object({
1213
article_id: z.string().optional(),
@@ -101,6 +102,17 @@ export const inngest = new Inngest({
101102
eventKey: process.env.INNGEST_EVENT_KEY ?? "local",
102103
});
103104

105+
export const cleanupExpiredArticlesFn = inngest.createFunction(
106+
{
107+
id: "cleanup-expired-articles",
108+
triggers: [{ cron: "0 2 * * *" }],
109+
},
110+
async () => {
111+
const result = await deleteExpiredArticles();
112+
return { deletedCount: result.deletedCount };
113+
},
114+
);
115+
104116
export const persistNotificationFn = inngest.createFunction(
105117
{
106118
id: "persist-notification",

wrangler.toml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,5 @@ compatibility_date = "2024-01-01"
33
main = "src/workers/cron-worker.ts"
44
# R2 bucket configuration
55

6-
# Cron triggers configuration
7-
[triggers]
8-
crons = [
9-
# Run article cleanup every day at 2:00 AM UTC
10-
"0 2 * * *",
11-
# "* * * * *", # Uncomment for production use
12-
]
13-
146
[observability.logs]
157
enabled = true

0 commit comments

Comments
 (0)