diff --git a/.env.example b/.env.example index 0fbf303..798a6ca 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,20 @@ # Runtime NODE_ENV=production PORT=3000 -STATE_FILE_PATH=/data/state.json LOG_LEVEL=info APP_BASE_URL=https://xartaudio.example.com +# Better Auth +BETTER_AUTH_SECRET=replace-me +BETTER_AUTH_BASE_PATH=/api/auth +BETTER_AUTH_DEV_PASSWORD=replace-me + +# Convex +CONVEX_DEPLOYMENT_URL=https://your-deployment.convex.cloud +CONVEX_AUTH_TOKEN= +CONVEX_STATE_QUERY=state:getLatestSnapshot +CONVEX_STATE_MUTATION=state:saveSnapshot + # Webhook secrets X_WEBHOOK_SECRET=replace-me POLAR_WEBHOOK_SECRET=replace-me @@ -18,19 +28,22 @@ 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 +# Qwen3 TTS +QWEN_TTS_API_KEY=replace-me +QWEN_TTS_BASE_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1 +QWEN_TTS_MODEL=qwen-tts-latest +QWEN_TTS_VOICE=Cherry +QWEN_TTS_FORMAT=mp3 -# 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 +# MinIO object storage +MINIO_ENDPOINT=minio.example.com +MINIO_PORT=443 +MINIO_USE_SSL=true +MINIO_BUCKET=replace-me +MINIO_REGION=us-east-1 +MINIO_ACCESS_KEY=replace-me +MINIO_SECRET_KEY=replace-me +MINIO_SIGNED_URL_TTL_SEC=3600 # Credit policy BASE_CREDITS=1 diff --git a/Dockerfile b/Dockerfile index b7bd935..0bbb522 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 -ENV STATE_FILE_PATH=/data/state.json +ENV CONVEX_DEPLOYMENT_URL= COPY package.json bun.lock ./ RUN bun install --frozen-lockfile @@ -15,7 +15,6 @@ COPY spec.md ./spec.md RUN bun run build:css EXPOSE 3000 -VOLUME ["/data"] HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD bun -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" diff --git a/README.md b/README.md index 1ab0292..e020ee8 100644 --- a/README.md +++ b/README.md @@ -330,10 +330,10 @@ This repository now contains a deployable production-style app (single container 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`) +- TTS (`Qwen3 TTS`, OpenAI-compatible endpoint via `fetch`) +- Object storage + signed URLs (`minio`) 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 through Convex query/mutation functions 7. Abuse protection: - fixed-window rate limiting for webhook, auth, and action routes 8. PWA support: @@ -344,9 +344,9 @@ This repository now contains a deployable production-style app (single container - `bun run lint` ### Authentication model -1. Browser flow uses secure-ish HTTP-only cookie session (`xartaudio_user`) via `/auth/dev-login`. -2. API calls also support `x-user-id` header for scripted usage/testing. -3. This auth layer is intentionally replaceable with X OAuth in production rollout. +1. Browser flow is powered by Better Auth under `/api/auth/*`. +2. `/auth/dev-login` bootstraps a Better Auth session for local/dev testing. +3. API calls also support `x-user-id` header for scripted usage/testing. ### Runtime endpoints 1. Public: @@ -380,10 +380,17 @@ Use `.env.example` as the source of truth. 1. Runtime: - `PORT` -- `STATE_FILE_PATH` - `LOG_LEVEL` - `APP_BASE_URL` -2. Secrets: +2. Auth + state: +- `BETTER_AUTH_SECRET` +- `BETTER_AUTH_BASE_PATH` +- `BETTER_AUTH_DEV_PASSWORD` +- `CONVEX_DEPLOYMENT_URL` +- `CONVEX_AUTH_TOKEN` +- `CONVEX_STATE_QUERY` +- `CONVEX_STATE_MUTATION` +3. Secrets: - `X_WEBHOOK_SECRET` - `POLAR_WEBHOOK_SECRET` - `X_BEARER_TOKEN` @@ -391,23 +398,26 @@ Use `.env.example` as the source of truth. - `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: +- `QWEN_TTS_API_KEY` +- `QWEN_TTS_BASE_URL` +- `QWEN_TTS_MODEL` +- `QWEN_TTS_VOICE` +- `QWEN_TTS_FORMAT` +- `MINIO_ENDPOINT` +- `MINIO_PORT` +- `MINIO_USE_SSL` +- `MINIO_BUCKET` +- `MINIO_REGION` +- `MINIO_ACCESS_KEY` +- `MINIO_SECRET_KEY` +- `MINIO_SIGNED_URL_TTL_SEC` +4. Credit model: - `BASE_CREDITS` - `INCLUDED_CHARS` - `STEP_CHARS` - `STEP_CREDITS` - `MAX_CHARS_PER_ARTICLE` -4. Rate limits: +5. Rate limits: - `WEBHOOK_RPM` - `AUTH_RPM` - `ACTION_RPM` @@ -416,17 +426,16 @@ Use `.env.example` as the source of truth. 1. Create a new service from this repository and select `Dockerfile` build mode. 2. Set container port to `3000`. -3. Add a persistent volume mounted at `/data`. -4. Set `STATE_FILE_PATH=/data/state.json`. -5. Configure all secrets and policy env vars from `.env.example`. -6. Expose HTTPS URL and point providers to: +3. Configure all secrets and policy env vars from `.env.example`. +4. Ensure `CONVEX_DEPLOYMENT_URL` is reachable from the container network. +5. Expose HTTPS URL and point providers to: - `https:///api/webhooks/x` - `https:///api/webhooks/polar` -7. Verify deployment health with `GET /health`. +6. Verify deployment health with `GET /health`. ## Production Checklist -1. Replace dev-login cookie auth with X OAuth before public launch. -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. +1. Replace `/auth/dev-login` with direct Better Auth UI/OAuth sign-in for public launch. +2. Populate integration keys in Coolify environment for X, Polar, Qwen3 TTS, MinIO, and Convex. +3. Implement Convex functions named by `CONVEX_STATE_QUERY` and `CONVEX_STATE_MUTATION`. 4. Add tracing and external alerting. diff --git a/test/deployment.test.js b/test/deployment.test.js index 2e94e56..606e808 100644 --- a/test/deployment.test.js +++ b/test/deployment.test.js @@ -8,7 +8,7 @@ test("Dockerfile contains production container essentials", () => { const dockerfile = fs.readFileSync("Dockerfile", "utf8"); assert.match(dockerfile, /FROM oven\/bun:1\.3\.5-alpine/); assert.match(dockerfile, /EXPOSE 3000/); - assert.match(dockerfile, /STATE_FILE_PATH=\/data\/state\.json/); + assert.match(dockerfile, /CONVEX_DEPLOYMENT_URL=/); assert.match(dockerfile, /bun install --frozen-lockfile/); assert.match(dockerfile, /bun run build:css/); assert.match(dockerfile, /HEALTHCHECK/); @@ -25,8 +25,10 @@ test("env example includes required webhook and credit settings", () => { 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, /QWEN_TTS_API_KEY=/); + assert.match(envFile, /MINIO_ENDPOINT=/); + assert.match(envFile, /CONVEX_DEPLOYMENT_URL=/); + assert.match(envFile, /BETTER_AUTH_SECRET=/); assert.match(envFile, /INCLUDED_CHARS=/); assert.match(envFile, /WEBHOOK_RPM=/); });