feat: expand production config and documentation for provider integrations

This commit is contained in:
Codex
2026-02-18 13:35:19 +00:00
parent 74ab63f488
commit 415c9d165a
5 changed files with 217 additions and 6 deletions

View File

@@ -3,11 +3,35 @@ NODE_ENV=production
PORT=3000 PORT=3000
STATE_FILE_PATH=/data/state.json STATE_FILE_PATH=/data/state.json
LOG_LEVEL=info LOG_LEVEL=info
APP_BASE_URL=https://xartaudio.example.com
# Webhook secrets # Webhook secrets
X_WEBHOOK_SECRET=replace-me X_WEBHOOK_SECRET=replace-me
POLAR_WEBHOOK_SECRET=replace-me POLAR_WEBHOOK_SECRET=replace-me
# X API
X_BEARER_TOKEN=replace-me
X_BOT_USER_ID=replace-me
# Polar API
POLAR_ACCESS_TOKEN=replace-me
POLAR_SERVER=production
POLAR_PRODUCT_IDS=prod_123
# TTS (OpenAI-compatible)
TTS_API_KEY=replace-me
TTS_BASE_URL=
TTS_MODEL=gpt-4o-mini-tts
TTS_VOICE=alloy
# S3-compatible object storage
S3_BUCKET=replace-me
S3_REGION=us-east-1
S3_ENDPOINT=
S3_ACCESS_KEY_ID=replace-me
S3_SECRET_ACCESS_KEY=replace-me
S3_SIGNED_URL_TTL_SEC=3600
# Credit policy # Credit policy
BASE_CREDITS=1 BASE_CREDITS=1
INCLUDED_CHARS=25000 INCLUDED_CHARS=25000

View File

@@ -326,14 +326,22 @@ This repository now contains a deployable production-style app (single container
- non-owner pay-to-unlock (same credit amount, permanent unlock) - non-owner pay-to-unlock (same credit amount, permanent unlock)
4. Webhook-first ingestion and billing: 4. Webhook-first ingestion and billing:
- `POST /api/webhooks/x` (HMAC verified) - `POST /api/webhooks/x` (HMAC verified)
- `POST /api/webhooks/polar` (HMAC verified) - `POST /api/webhooks/polar` (supports Polar standard webhook signatures and legacy HMAC fallback)
5. Persistent state across restarts: 5. Real integration adapters implemented:
- X API (`twitter-api-v2`)
- Polar SDK checkout/webhook handling (`@polar-sh/sdk`)
- TTS (`openai`)
- Object storage + signed URLs (`@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`)
6. Persistent state across restarts:
- all wallet/job/asset/access state is snapshotted and stored to `STATE_FILE_PATH` - all wallet/job/asset/access state is snapshotted and stored to `STATE_FILE_PATH`
6. Abuse protection: 7. Abuse protection:
- fixed-window rate limiting for webhook, auth, and action routes - fixed-window rate limiting for webhook, auth, and action routes
7. PWA support: 8. PWA support:
- `manifest.webmanifest` - `manifest.webmanifest`
- `sw.js` - `sw.js`
9. Bun-native quality checks:
- `bun test`
- `bun run lint`
### Authentication model ### Authentication model
1. Browser flow uses secure-ish HTTP-only cookie session (`xartaudio_user`) via `/auth/dev-login`. 1. Browser flow uses secure-ish HTTP-only cookie session (`xartaudio_user`) via `/auth/dev-login`.
@@ -354,6 +362,8 @@ This repository now contains a deployable production-style app (single container
3. APIs: 3. APIs:
- `POST /api/webhooks/x` - `POST /api/webhooks/x`
- `POST /api/webhooks/polar` - `POST /api/webhooks/polar`
- `POST /api/payments/create-checkout`
- `GET /api/x/mentions`
- `GET /api/me/wallet` - `GET /api/me/wallet`
- `GET /api/jobs/:id` - `GET /api/jobs/:id`
- `POST /api/audio/:id/unlock` - `POST /api/audio/:id/unlock`
@@ -371,9 +381,26 @@ Use `.env.example` as the source of truth.
1. Runtime: 1. Runtime:
- `PORT` - `PORT`
- `STATE_FILE_PATH` - `STATE_FILE_PATH`
- `LOG_LEVEL`
- `APP_BASE_URL`
2. Secrets: 2. Secrets:
- `X_WEBHOOK_SECRET` - `X_WEBHOOK_SECRET`
- `POLAR_WEBHOOK_SECRET` - `POLAR_WEBHOOK_SECRET`
- `X_BEARER_TOKEN`
- `X_BOT_USER_ID`
- `POLAR_ACCESS_TOKEN`
- `POLAR_SERVER`
- `POLAR_PRODUCT_IDS`
- `TTS_API_KEY`
- `TTS_BASE_URL`
- `TTS_MODEL`
- `TTS_VOICE`
- `S3_BUCKET`
- `S3_REGION`
- `S3_ENDPOINT`
- `S3_ACCESS_KEY_ID`
- `S3_SECRET_ACCESS_KEY`
- `S3_SIGNED_URL_TTL_SEC`
3. Credit model: 3. Credit model:
- `BASE_CREDITS` - `BASE_CREDITS`
- `INCLUDED_CHARS` - `INCLUDED_CHARS`
@@ -400,6 +427,6 @@ Use `.env.example` as the source of truth.
## Production Checklist ## Production Checklist
1. Replace dev-login cookie auth with X OAuth before public launch. 1. Replace dev-login cookie auth with X OAuth before public launch.
2. Connect real TTS generation worker and object storage (S3/R2/GCS). 2. Populate integration keys in Coolify environment for X, Polar, TTS, and S3.
3. Replace local state file with managed database for multi-replica scaling. 3. Replace local state file with managed database for multi-replica scaling.
4. Add structured logging, tracing, and external alerting. 4. Add tracing and external alerting.

View File

@@ -19,12 +19,40 @@ function strFromEnv(name, fallback) {
return raw && String(raw).trim() ? String(raw).trim() : fallback; return raw && String(raw).trim() ? String(raw).trim() : fallback;
} }
function listFromEnv(name, fallback = []) {
const raw = process.env[name];
if (!raw) {
return fallback;
}
return String(raw)
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
const parsed = { const parsed = {
port: intFromEnv("PORT", 3000), port: intFromEnv("PORT", 3000),
stateFilePath: strFromEnv("STATE_FILE_PATH", "./data/state.json"), stateFilePath: strFromEnv("STATE_FILE_PATH", "./data/state.json"),
logLevel: strFromEnv("LOG_LEVEL", "info"), logLevel: strFromEnv("LOG_LEVEL", "info"),
appBaseUrl: strFromEnv("APP_BASE_URL", "http://localhost:3000"),
xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret", xWebhookSecret: process.env.X_WEBHOOK_SECRET || "dev-x-secret",
xBearerToken: strFromEnv("X_BEARER_TOKEN", ""),
xBotUserId: strFromEnv("X_BOT_USER_ID", ""),
polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret", polarWebhookSecret: process.env.POLAR_WEBHOOK_SECRET || "dev-polar-secret",
polarAccessToken: strFromEnv("POLAR_ACCESS_TOKEN", ""),
polarServer: strFromEnv("POLAR_SERVER", "production"),
polarProductIds: listFromEnv("POLAR_PRODUCT_IDS", []),
ttsApiKey: strFromEnv("TTS_API_KEY", ""),
ttsBaseUrl: strFromEnv("TTS_BASE_URL", ""),
ttsModel: strFromEnv("TTS_MODEL", "gpt-4o-mini-tts"),
ttsVoice: strFromEnv("TTS_VOICE", "alloy"),
s3Bucket: strFromEnv("S3_BUCKET", ""),
s3Region: strFromEnv("S3_REGION", ""),
s3Endpoint: strFromEnv("S3_ENDPOINT", ""),
s3AccessKeyId: strFromEnv("S3_ACCESS_KEY_ID", ""),
s3SecretAccessKey: strFromEnv("S3_SECRET_ACCESS_KEY", ""),
s3SignedUrlTtlSec: intFromEnv("S3_SIGNED_URL_TTL_SEC", 3600),
rateLimits: { rateLimits: {
webhookPerMinute: intFromEnv("WEBHOOK_RPM", 120), webhookPerMinute: intFromEnv("WEBHOOK_RPM", 120),
authPerMinute: intFromEnv("AUTH_RPM", 30), authPerMinute: intFromEnv("AUTH_RPM", 30),
@@ -43,8 +71,24 @@ const ConfigSchema = z.object({
port: z.number().int().positive(), port: z.number().int().positive(),
stateFilePath: z.string().min(1), stateFilePath: z.string().min(1),
logLevel: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]), logLevel: z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]),
appBaseUrl: z.string().min(1),
xWebhookSecret: z.string().min(1), xWebhookSecret: z.string().min(1),
xBearerToken: z.string(),
xBotUserId: z.string(),
polarWebhookSecret: z.string().min(1), polarWebhookSecret: z.string().min(1),
polarAccessToken: z.string(),
polarServer: z.enum(["production", "sandbox"]),
polarProductIds: z.array(z.string().min(1)),
ttsApiKey: z.string(),
ttsBaseUrl: z.string(),
ttsModel: z.string().min(1),
ttsVoice: z.string().min(1),
s3Bucket: z.string(),
s3Region: z.string(),
s3Endpoint: z.string(),
s3AccessKeyId: z.string(),
s3SecretAccessKey: z.string(),
s3SignedUrlTtlSec: z.number().int().positive(),
rateLimits: z.object({ rateLimits: z.object({
webhookPerMinute: z.number().int().positive(), webhookPerMinute: z.number().int().positive(),
authPerMinute: z.number().int().positive(), authPerMinute: z.number().int().positive(),

View File

@@ -8,13 +8,25 @@ test("config uses defaults when env is missing", () => {
PORT: process.env.PORT, PORT: process.env.PORT,
STATE_FILE_PATH: process.env.STATE_FILE_PATH, STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL, LOG_LEVEL: process.env.LOG_LEVEL,
APP_BASE_URL: process.env.APP_BASE_URL,
TTS_MODEL: process.env.TTS_MODEL,
S3_SIGNED_URL_TTL_SEC: process.env.S3_SIGNED_URL_TTL_SEC,
X_BOT_USER_ID: process.env.X_BOT_USER_ID,
WEBHOOK_RPM: process.env.WEBHOOK_RPM, WEBHOOK_RPM: process.env.WEBHOOK_RPM,
POLAR_SERVER: process.env.POLAR_SERVER,
POLAR_PRODUCT_IDS: process.env.POLAR_PRODUCT_IDS,
}; };
delete process.env.PORT; delete process.env.PORT;
delete process.env.STATE_FILE_PATH; delete process.env.STATE_FILE_PATH;
delete process.env.LOG_LEVEL; delete process.env.LOG_LEVEL;
delete process.env.APP_BASE_URL;
delete process.env.TTS_MODEL;
delete process.env.S3_SIGNED_URL_TTL_SEC;
delete process.env.X_BOT_USER_ID;
delete process.env.WEBHOOK_RPM; delete process.env.WEBHOOK_RPM;
delete process.env.POLAR_SERVER;
delete process.env.POLAR_PRODUCT_IDS;
delete require.cache[require.resolve("../src/config")]; delete require.cache[require.resolve("../src/config")];
const { config } = require("../src/config"); const { config } = require("../src/config");
@@ -22,6 +34,12 @@ test("config uses defaults when env is missing", () => {
assert.equal(config.port, 3000); assert.equal(config.port, 3000);
assert.equal(config.stateFilePath, "./data/state.json"); assert.equal(config.stateFilePath, "./data/state.json");
assert.equal(config.logLevel, "info"); assert.equal(config.logLevel, "info");
assert.equal(config.appBaseUrl, "http://localhost:3000");
assert.equal(config.ttsModel, "gpt-4o-mini-tts");
assert.equal(config.s3SignedUrlTtlSec, 3600);
assert.equal(config.xBotUserId, "");
assert.equal(config.polarServer, "production");
assert.deepEqual(config.polarProductIds, []);
assert.equal(config.rateLimits.webhookPerMinute, 120); assert.equal(config.rateLimits.webhookPerMinute, 120);
if (previous.PORT === undefined) { if (previous.PORT === undefined) {
@@ -42,11 +60,47 @@ test("config uses defaults when env is missing", () => {
process.env.LOG_LEVEL = previous.LOG_LEVEL; process.env.LOG_LEVEL = previous.LOG_LEVEL;
} }
if (previous.APP_BASE_URL === undefined) {
delete process.env.APP_BASE_URL;
} else {
process.env.APP_BASE_URL = previous.APP_BASE_URL;
}
if (previous.TTS_MODEL === undefined) {
delete process.env.TTS_MODEL;
} else {
process.env.TTS_MODEL = previous.TTS_MODEL;
}
if (previous.S3_SIGNED_URL_TTL_SEC === undefined) {
delete process.env.S3_SIGNED_URL_TTL_SEC;
} else {
process.env.S3_SIGNED_URL_TTL_SEC = previous.S3_SIGNED_URL_TTL_SEC;
}
if (previous.X_BOT_USER_ID === undefined) {
delete process.env.X_BOT_USER_ID;
} else {
process.env.X_BOT_USER_ID = previous.X_BOT_USER_ID;
}
if (previous.WEBHOOK_RPM === undefined) { if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM; delete process.env.WEBHOOK_RPM;
} else { } else {
process.env.WEBHOOK_RPM = previous.WEBHOOK_RPM; process.env.WEBHOOK_RPM = previous.WEBHOOK_RPM;
} }
if (previous.POLAR_SERVER === undefined) {
delete process.env.POLAR_SERVER;
} else {
process.env.POLAR_SERVER = previous.POLAR_SERVER;
}
if (previous.POLAR_PRODUCT_IDS === undefined) {
delete process.env.POLAR_PRODUCT_IDS;
} else {
process.env.POLAR_PRODUCT_IDS = previous.POLAR_PRODUCT_IDS;
}
}); });
test("config reads state path and numeric env overrides", () => { test("config reads state path and numeric env overrides", () => {
@@ -55,12 +109,24 @@ test("config reads state path and numeric env overrides", () => {
STATE_FILE_PATH: process.env.STATE_FILE_PATH, STATE_FILE_PATH: process.env.STATE_FILE_PATH,
LOG_LEVEL: process.env.LOG_LEVEL, LOG_LEVEL: process.env.LOG_LEVEL,
WEBHOOK_RPM: process.env.WEBHOOK_RPM, WEBHOOK_RPM: process.env.WEBHOOK_RPM,
APP_BASE_URL: process.env.APP_BASE_URL,
TTS_MODEL: process.env.TTS_MODEL,
S3_SIGNED_URL_TTL_SEC: process.env.S3_SIGNED_URL_TTL_SEC,
X_BOT_USER_ID: process.env.X_BOT_USER_ID,
POLAR_SERVER: process.env.POLAR_SERVER,
POLAR_PRODUCT_IDS: process.env.POLAR_PRODUCT_IDS,
}; };
process.env.PORT = "8080"; process.env.PORT = "8080";
process.env.STATE_FILE_PATH = "/data/prod-state.json"; process.env.STATE_FILE_PATH = "/data/prod-state.json";
process.env.LOG_LEVEL = "debug"; process.env.LOG_LEVEL = "debug";
process.env.APP_BASE_URL = "https://xartaudio.app";
process.env.TTS_MODEL = "custom-tts";
process.env.S3_SIGNED_URL_TTL_SEC = "7200";
process.env.X_BOT_USER_ID = "bot-user-id";
process.env.WEBHOOK_RPM = "77"; process.env.WEBHOOK_RPM = "77";
process.env.POLAR_SERVER = "sandbox";
process.env.POLAR_PRODUCT_IDS = "prod_1,prod_2";
delete require.cache[require.resolve("../src/config")]; delete require.cache[require.resolve("../src/config")];
const { config } = require("../src/config"); const { config } = require("../src/config");
@@ -68,6 +134,12 @@ test("config reads state path and numeric env overrides", () => {
assert.equal(config.port, 8080); assert.equal(config.port, 8080);
assert.equal(config.stateFilePath, "/data/prod-state.json"); assert.equal(config.stateFilePath, "/data/prod-state.json");
assert.equal(config.logLevel, "debug"); assert.equal(config.logLevel, "debug");
assert.equal(config.appBaseUrl, "https://xartaudio.app");
assert.equal(config.ttsModel, "custom-tts");
assert.equal(config.s3SignedUrlTtlSec, 7200);
assert.equal(config.xBotUserId, "bot-user-id");
assert.equal(config.polarServer, "sandbox");
assert.deepEqual(config.polarProductIds, ["prod_1", "prod_2"]);
assert.equal(config.rateLimits.webhookPerMinute, 77); assert.equal(config.rateLimits.webhookPerMinute, 77);
if (previous.PORT === undefined) { if (previous.PORT === undefined) {
@@ -86,9 +158,46 @@ test("config reads state path and numeric env overrides", () => {
} else { } else {
process.env.LOG_LEVEL = previous.LOG_LEVEL; process.env.LOG_LEVEL = previous.LOG_LEVEL;
} }
if (previous.APP_BASE_URL === undefined) {
delete process.env.APP_BASE_URL;
} else {
process.env.APP_BASE_URL = previous.APP_BASE_URL;
}
if (previous.TTS_MODEL === undefined) {
delete process.env.TTS_MODEL;
} else {
process.env.TTS_MODEL = previous.TTS_MODEL;
}
if (previous.S3_SIGNED_URL_TTL_SEC === undefined) {
delete process.env.S3_SIGNED_URL_TTL_SEC;
} else {
process.env.S3_SIGNED_URL_TTL_SEC = previous.S3_SIGNED_URL_TTL_SEC;
}
if (previous.X_BOT_USER_ID === undefined) {
delete process.env.X_BOT_USER_ID;
} else {
process.env.X_BOT_USER_ID = previous.X_BOT_USER_ID;
}
if (previous.WEBHOOK_RPM === undefined) { if (previous.WEBHOOK_RPM === undefined) {
delete process.env.WEBHOOK_RPM; delete process.env.WEBHOOK_RPM;
} else { } else {
process.env.WEBHOOK_RPM = previous.WEBHOOK_RPM; process.env.WEBHOOK_RPM = previous.WEBHOOK_RPM;
} }
if (previous.POLAR_SERVER === undefined) {
delete process.env.POLAR_SERVER;
} else {
process.env.POLAR_SERVER = previous.POLAR_SERVER;
}
if (previous.POLAR_PRODUCT_IDS === undefined) {
delete process.env.POLAR_PRODUCT_IDS;
} else {
process.env.POLAR_PRODUCT_IDS = previous.POLAR_PRODUCT_IDS;
}
}); });

View File

@@ -17,8 +17,15 @@ test("Dockerfile contains production container essentials", () => {
test("env example includes required webhook and credit settings", () => { test("env example includes required webhook and credit settings", () => {
const envFile = fs.readFileSync(".env.example", "utf8"); const envFile = fs.readFileSync(".env.example", "utf8");
assert.match(envFile, /LOG_LEVEL=/); assert.match(envFile, /LOG_LEVEL=/);
assert.match(envFile, /APP_BASE_URL=/);
assert.match(envFile, /X_WEBHOOK_SECRET=/); assert.match(envFile, /X_WEBHOOK_SECRET=/);
assert.match(envFile, /X_BEARER_TOKEN=/);
assert.match(envFile, /X_BOT_USER_ID=/);
assert.match(envFile, /POLAR_WEBHOOK_SECRET=/); assert.match(envFile, /POLAR_WEBHOOK_SECRET=/);
assert.match(envFile, /POLAR_ACCESS_TOKEN=/);
assert.match(envFile, /POLAR_PRODUCT_IDS=/);
assert.match(envFile, /TTS_API_KEY=/);
assert.match(envFile, /S3_BUCKET=/);
assert.match(envFile, /INCLUDED_CHARS=/); assert.match(envFile, /INCLUDED_CHARS=/);
assert.match(envFile, /WEBHOOK_RPM=/); assert.match(envFile, /WEBHOOK_RPM=/);
}); });