Skip to content

Commit 62d7c44

Browse files
authored
feat: add REDIS_KEY_PREFIX env var for Redis ACL namespace isolation (#365)
* feat: add REDIS_KEY_PREFIX env var for Redis ACL namespace isolation Adds optional REDIS_KEY_PREFIX env var that prefixes all Redis keys (BullMQ queues via `prefix` option, cache/lock/rate-limit keys via `redisKey()` helper). When unset, behavior is unchanged (BullMQ defaults to "bull:", cache keys are unprefixed). This enables self-hosters using Redis ACL multi-tenancy to restrict useSend to its own key namespace (e.g. `~usesend:*`). 16 files changed across env schema, Redis module, 9 BullMQ queue/worker files, and 5 direct Redis key operation sites. * docs: add REDIS_KEY_PREFIX to self-host assets and fix docker run example Add REDIS_KEY_PREFIX env var to docker/prod/compose.yml, .env.example, .env.selfhost.example, and self-hosting docs. Fix missing trailing backslashes in standalone docker run example. * fix(redis): disable ioredis ready check and BullMQ version check Redis ACL blocks INFO command (in @dangerous category). ioredis uses INFO for ready check, BullMQ uses it for version detection. Without these flags, BullMQ workers fail to initialize and silently stop processing jobs. - Add enableReadyCheck: false to ioredis connection - Add skipVersionCheck: true to all 5 Queue + 5 Worker constructors * fix(redis): add skipVersionCheck to remaining BullMQ job queues Add skipVersionCheck: true to Queue and Worker constructors in all 4 job files (campaign-scheduler, cleanup-email-bodies, usage-job, webhook-cleanup) to match the pattern already used in service files. This prevents BullMQ version mismatch errors when using REDIS_KEY_PREFIX with Redis ACL namespace isolation.
1 parent 69eeb2d commit 62d7c44

21 files changed

Lines changed: 100 additions & 37 deletions

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,7 @@ FROM_EMAIL="hello@usesend.com"
2222
API_RATE_LIMIT=2
2323
AUTH_EMAIL_RATE_LIMIT=5
2424

25+
# Optional: prefix all Redis keys (useful for shared Redis with ACL isolation)
26+
# REDIS_KEY_PREFIX=""
27+
2528
NEXT_PUBLIC_IS_CLOUD=true

.env.selfhost.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,8 @@ DOCKER_OUTPUT=1
3636
API_RATE_LIMIT=1
3737
AUTH_EMAIL_RATE_LIMIT=5
3838

39+
# Optional: prefix all Redis keys (useful for shared Redis with ACL isolation)
40+
# REDIS_KEY_PREFIX=""
41+
3942
# used to send important error notification - optional
4043
DISCORD_WEBHOOK_URL=""

apps/docs/self-hosting/overview.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ DATABASE_URL="postgres://<username>:<password>@<host>:<port>/<database-name>"
5959
REDIS_URL="redis://<username>:<password>@<host>:<port>"
6060
```
6161

62+
If you're sharing a Redis instance across multiple apps and using Redis ACL for isolation, set `REDIS_KEY_PREFIX` to namespace all keys (e.g. `REDIS_KEY_PREFIX="usesend"` prefixes all keys with `usesend:`).
63+
6264
</Step>
6365
<Step title="Next auth url and secret">
6466
Url is the app url you're going to use and secret is random string. You can generate a random secret using this command.

apps/web/src/app/api/auth/[...nextauth]/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import NextAuth from "next-auth";
22

33
import { authOptions } from "~/server/auth";
44
import { env } from "~/env";
5-
import { getRedis } from "~/server/redis";
5+
import { getRedis, redisKey } from "~/server/redis";
66
import { logger } from "~/server/logger/log";
77

88
const handler = NextAuth(authOptions);
@@ -60,7 +60,7 @@ export async function POST(req: Request, ctx: any) {
6060
return handler(req, ctx);
6161
}
6262
const redis = getRedis();
63-
const key = `auth-rl:${ip}`;
63+
const key = redisKey(`auth-rl:${ip}`);
6464
const ttl = 60;
6565
const count = await redis.incr(key);
6666
if (count === 1) await redis.expire(key, ttl);

apps/web/src/env.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export const env = createEnv({
5353
FOUNDER_EMAIL: z.string().optional(),
5454
DISCORD_WEBHOOK_URL: z.string().optional(),
5555
REDIS_URL: z.string(),
56+
REDIS_KEY_PREFIX: z.string().default(""),
5657
S3_COMPATIBLE_ACCESS_KEY: z.string().optional(),
5758
S3_COMPATIBLE_SECRET_KEY: z.string().optional(),
5859
S3_COMPATIBLE_API_URL: z.string().optional(),
@@ -116,6 +117,7 @@ export const env = createEnv({
116117
FOUNDER_EMAIL: process.env.FOUNDER_EMAIL,
117118
DISCORD_WEBHOOK_URL: process.env.DISCORD_WEBHOOK_URL,
118119
REDIS_URL: process.env.REDIS_URL,
120+
REDIS_KEY_PREFIX: process.env.REDIS_KEY_PREFIX,
119121
FROM_EMAIL: process.env.FROM_EMAIL,
120122
S3_COMPATIBLE_ACCESS_KEY: process.env.S3_COMPATIBLE_ACCESS_KEY,
121123
S3_COMPATIBLE_SECRET_KEY: process.env.S3_COMPATIBLE_SECRET_KEY,

apps/web/src/server/api/routers/waitlist.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { env } from "~/env";
44
import { authedProcedure, createTRPCRouter } from "~/server/api/trpc";
55
import { logger } from "~/server/logger/log";
66
import { sendMail } from "~/server/mailer";
7-
import { getRedis } from "~/server/redis";
7+
import { getRedis, redisKey } from "~/server/redis";
88
import {
99
WAITLIST_EMAIL_TYPES,
1010
waitlistSubmissionSchema,
@@ -40,7 +40,7 @@ export const waitlistRouter = createTRPCRouter({
4040
}
4141

4242
const redis = getRedis();
43-
const rateKey = `waitlist:requests:${user.id}`;
43+
const rateKey = redisKey(`waitlist:requests:${user.id}`);
4444

4545
const currentCountRaw = await redis.get(rateKey);
4646
const currentCount = currentCountRaw ? Number(currentCountRaw) : 0;

apps/web/src/server/jobs/campaign-scheduler-job.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
CAMPAIGN_SCHEDULER_QUEUE,
55
DEFAULT_QUEUE_OPTIONS,
66
} from "../queue/queue-constants";
7-
import { getRedis } from "../redis";
7+
import { getRedis, BULL_PREFIX } from "../redis";
88
import { CampaignBatchService } from "../service/campaign-service";
99
import { db } from "../db";
1010
import { logger } from "../logger/log";
@@ -18,6 +18,8 @@ export class CampaignSchedulerService {
1818
CAMPAIGN_SCHEDULER_QUEUE,
1919
{
2020
connection: getRedis(),
21+
prefix: BULL_PREFIX,
22+
skipVersionCheck: true,
2123
}
2224
);
2325

@@ -82,7 +84,7 @@ export class CampaignSchedulerService {
8284
logger.error({ err }, "Campaign scheduler tick failed");
8385
}
8486
}),
85-
{ connection: getRedis(), concurrency: 1 }
87+
{ connection: getRedis(), concurrency: 1, prefix: BULL_PREFIX, skipVersionCheck: true }
8688
);
8789

8890
static async start() {

apps/web/src/server/jobs/cleanup-email-bodies.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Queue, Worker} from "bullmq";
22
import {db} from "~/server/db";
3-
import {getRedis} from "~/server/redis";
3+
import {getRedis, BULL_PREFIX} from "~/server/redis";
44
import {logger} from "../logger/log";
55
import {DEFAULT_QUEUE_OPTIONS} from "../queue/queue-constants";
66
import {env} from "~/env";
@@ -19,6 +19,8 @@ if (isSelfHosted() && isEmailCleanupEnabled()) {
1919
*/
2020
const cleanupQueue = new Queue(CLEANUP_QUEUE_NAME, {
2121
connection: getRedis(),
22+
prefix: BULL_PREFIX,
23+
skipVersionCheck: true,
2224
});
2325

2426
const worker = new Worker(
@@ -47,6 +49,8 @@ if (isSelfHosted() && isEmailCleanupEnabled()) {
4749
},
4850
{
4951
connection: getRedis(),
52+
prefix: BULL_PREFIX,
53+
skipVersionCheck: true,
5054
}
5155
);
5256

apps/web/src/server/jobs/usage-job.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import { db } from "~/server/db";
33
import { env } from "~/env";
44
import { getUsageDate, getUsageUnits } from "~/lib/usage";
55
import { sendUsageToStripe } from "~/server/billing/usage";
6-
import { getRedis } from "~/server/redis";
6+
import { getRedis, BULL_PREFIX } from "~/server/redis";
77
import { DEFAULT_QUEUE_OPTIONS } from "../queue/queue-constants";
88
import { logger } from "../logger/log";
99

1010
const USAGE_QUEUE_NAME = "usage-reporting";
1111

1212
const usageQueue = new Queue(USAGE_QUEUE_NAME, {
1313
connection: getRedis(),
14+
prefix: BULL_PREFIX,
15+
skipVersionCheck: true,
1416
});
1517

1618
const worker = new Worker(
@@ -69,6 +71,8 @@ const worker = new Worker(
6971
},
7072
{
7173
connection: getRedis(),
74+
prefix: BULL_PREFIX,
75+
skipVersionCheck: true,
7276
},
7377
);
7478

apps/web/src/server/jobs/webhook-cleanup-job.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { Queue, Worker } from "bullmq";
22
import { subDays } from "date-fns";
33
import { db } from "~/server/db";
4-
import { getRedis } from "~/server/redis";
4+
import { getRedis, BULL_PREFIX } from "~/server/redis";
55
import { DEFAULT_QUEUE_OPTIONS, WEBHOOK_CLEANUP_QUEUE } from "../queue/queue-constants";
66
import { logger } from "../logger/log";
77

88
const WEBHOOK_RETENTION_DAYS = 30;
99

1010
const webhookCleanupQueue = new Queue(WEBHOOK_CLEANUP_QUEUE, {
1111
connection: getRedis(),
12+
prefix: BULL_PREFIX,
13+
skipVersionCheck: true,
1214
});
1315

1416
const worker = new Worker(
@@ -30,6 +32,8 @@ const worker = new Worker(
3032
},
3133
{
3234
connection: getRedis(),
35+
prefix: BULL_PREFIX,
36+
skipVersionCheck: true,
3337
}
3438
);
3539

0 commit comments

Comments
 (0)