Codapult ships three infrastructure modules — file storage, background jobs, and notifications — each using the adapter pattern. Switch implementations via a single environment variable; no code changes required.
File Storage
Upload and serve files through a unified API. The storage adapter is selected by STORAGE_PROVIDER.
| Provider | Env Value | Best For |
| ---------------- | ----------------- | ----------------------- |
| Local filesystem | local (default) | Development |
| Amazon S3 | s3 | Production (AWS) |
| Cloudflare R2 | r2 | Production (Cloudflare) |
Upload API
POST /api/upload accepts multipart form data. Authentication is required.
const form = new FormData();
form.append('file', file);
const res = await fetch('/api/upload', {
method: 'POST',
body: form,
});
const { url } = await res.json();
Image uploads are automatically optimized (resized, compressed) before storage.
Switching to S3 or R2
- Install the AWS SDK (required only for S3/R2):
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
- Set the provider and credentials in
.env.local:
STORAGE_PROVIDER="s3" # or "r2"
S3_BUCKET="my-bucket"
S3_REGION="us-east-1" # use "auto" for R2
S3_ENDPOINT="" # required for R2 (e.g. https://<account>.r2.cloudflarestorage.com)
S3_ACCESS_KEY_ID="..."
S3_SECRET_ACCESS_KEY="..."
S3_PUBLIC_URL="" # optional — public URL prefix for uploaded files
Production: Always use S3 or R2. The local adapter stores files on disk and is not suitable for multi-instance deployments.
Programmatic Usage
import { uploadFile, deleteFile } from '@/lib/storage';
const url = await uploadFile('avatars/user-123.webp', buffer, 'image/webp');
await deleteFile('avatars/user-123.webp');
Background Jobs
Offload work to background jobs for email sending, webhook delivery, credit resets, and RAG indexing. The job adapter is selected by JOB_PROVIDER.
| Provider | Env Value | Best For |
| --------------- | ------------------ | ---------------------------- |
| In-memory queue | memory (default) | Development, single-instance |
| BullMQ (Redis) | bullmq | Production, multi-instance |
Enqueuing Jobs
import { enqueue, enqueueEmail } from '@/lib/jobs';
// Generic job
await enqueue('webhook-retry', { webhookId: 'wh_123', attempt: 1 });
// Shorthand for emails
await enqueueEmail('[email protected]', 'Welcome!', emailHtml);
Built-in Jobs
| Job Name | Description |
| --------------- | ---------------------------------------------------------- |
| send-email | Sends transactional email via Resend |
| webhook-retry | Retries failed webhook deliveries with exponential backoff |
| credit-reset | Resets monthly AI/usage credits for all organizations |
| rag-index | Indexes documents for AI RAG pipeline |
Built-in Cron Jobs
| Schedule | Task | | -------- | ----------------------------------------- | | Daily | Session cleanup (remove expired sessions) | | Monthly | Credit reset (reset usage quotas) |
Switching to BullMQ
Set the provider and Redis connection in .env.local:
JOB_PROVIDER="bullmq"
REDIS_URL="redis://localhost:6379"
For production Kubernetes or Docker deployments, run the worker as a separate process alongside your Next.js server. The Helm chart includes a dedicated worker Deployment.
Notifications
In-app notifications with real-time delivery. The transport is selected by NOTIFICATION_TRANSPORT.
| Transport | Env Value | Description |
| ------------------ | ---------------- | ------------------------------------------------------ |
| Polling | poll (default) | Periodic HTTP requests — works everywhere, zero config |
| Server-Sent Events | sse | One-way real-time stream from server to client |
| WebSocket | ws | Full-duplex real-time — requires a separate WS server |
Dashboard Integration
The NotificationBell component in the dashboard header shows unread count and a dropdown list. It works with all three transports automatically.
Creating Notifications
import { createNotification } from '@/lib/notifications';
await createNotification({
userId: 'user_abc',
type: 'info', // 'info' | 'success' | 'warning' | 'error'
title: 'Deployment complete',
message: 'Your app was deployed to production.',
link: '/dashboard/deployments/42',
});
Operations
| Function | Description |
| ------------------------ | ---------------------------------- |
| createNotification() | Create and deliver a notification |
| getUserNotifications() | List notifications (newest first) |
| getUnreadCount() | Count unread notifications |
| markAsRead() | Mark a single notification as read |
| markAllAsRead() | Mark all notifications as read |
WebSocket Configuration
When using the ws transport, configure the WebSocket server URL and port:
NOTIFICATION_TRANSPORT="ws"
NEXT_PUBLIC_WS_URL="ws://localhost:3001"
WS_PORT="3001"
Choosing a Transport
- Polling — simplest option, no infrastructure dependencies. Good for low-traffic apps.
- SSE — real-time without extra servers. One-way (server → client). Good default for production.
- WebSocket — full-duplex, lowest latency. Requires running a separate WS server process.