From d8855ffabcf0424ca58b22c448e6f68b0f73dffe Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 4 Nov 2025 11:55:45 +0800 Subject: [PATCH 1/5] cleanup + document server envs --- apps/web/app/api/waitlist/route.ts | 29 -------- apps/web/next.config.mjs | 2 +- packages/database/drizzle.config.ts | 5 +- packages/env/server.ts | 103 ++++++++++++++++++++-------- scripts/env-cli.js | 11 --- 5 files changed, 79 insertions(+), 71 deletions(-) delete mode 100644 apps/web/app/api/waitlist/route.ts diff --git a/apps/web/app/api/waitlist/route.ts b/apps/web/app/api/waitlist/route.ts deleted file mode 100644 index 7b0a2ab8e0..0000000000 --- a/apps/web/app/api/waitlist/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { serverEnv } from "@cap/env"; - -export async function POST(request: Request) { - const res = await request.json(); - const { email } = res; - - if (!email || typeof email !== "string") { - return new Response("Email is required and must be a string", { - status: 400, - }); - } - - await fetch("https://app.loops.so/api/v1/contacts/create", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${serverEnv().NEXT_LOOPS_KEY}`, - }, - body: JSON.stringify({ - email, - userGroup: "Waitlist", - source: "auth", - }), - }); - - return new Response("Success", { - status: 200, - }); -} diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 4a7ad64454..0a5d0cfb48 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -109,7 +109,7 @@ const nextConfig = { env: { appVersion: version, }, - // If the DOCKER_BUILD environment variable is set to true, we are output nextjs to standalone ready for docker deployment + // If the NEXT_PUBLIC_DOCKER_BUILD environment variable is set to true, we are output nextjs to standalone ready for docker deployment output: process.env.NEXT_PUBLIC_DOCKER_BUILD === "true" ? "standalone" : undefined, // webpack: (config) => { diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index ebd65b350c..8475d3a273 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -1,9 +1,8 @@ import type { Config } from "drizzle-kit"; -const URL = process.env.DATABASE_MIGRATION_URL ?? process.env.DATABASE_URL; +const URL = process.env.DATABASE_URL; -if (!URL) - throw new Error("DATABASE_URL or DATABASE_MIGRATION_URL must be set!"); +if (!URL) throw new Error("DATABASE_URL must be set!"); if (!URL?.startsWith("mysql://")) throw new Error( "DATABASE_URL must be a 'mysql://' URI. Drizzle Kit doesn't support the fetch adapter!", diff --git a/packages/env/server.ts b/packages/env/server.ts index eee4df5a8c..69d71c07f6 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -12,40 +12,86 @@ const boolString = (_default = false) => function createServerEnv() { return createEnv({ server: { - NODE_ENV: z.string(), - DATABASE_URL: z.string(), - WEB_URL: z.string(), - DATABASE_MIGRATION_URL: z.string().optional(), - DATABASE_ENCRYPTION_KEY: z.string().optional(), - S3_PATH_STYLE: boolString(true), + /// General configuration + DATABASE_URL: z.string().describe("MySQL database URL"), + WEB_URL: z + .string() + .describe("Public URL of the server eg. https://cap.so"), + NEXTAUTH_SECRET: z.string().describe("32 byte base64 string"), + NEXTAUTH_URL: z.string().describe("Should be the same as WEB_URL"), + DATABASE_ENCRYPTION_KEY: z + .string() + .optional() + .describe( + "32 byte hex string for encrypting values like AWS access keys", + ), + + // Cap uses Resend for email sending, including sending login code emails + RESEND_API_KEY: z.string().optional(), + RESEND_FROM_DOMAIN: z.string().optional(), + + /// S3 configuration + // Though they are prefixed with `CAP_AWS`, these don't have to be + // for AWS, and can instead be for any S3-compatible service CAP_AWS_BUCKET: z.string(), CAP_AWS_REGION: z.string(), - CAP_AWS_BUCKET_URL: z.string().optional(), CAP_AWS_ACCESS_KEY: z.string().optional(), CAP_AWS_SECRET_KEY: z.string().optional(), - CAP_AWS_ENDPOINT: z.string().optional(), - CAP_AWS_MEDIACONVERT_ROLE_ARN: z.string().optional(), + S3_PUBLIC_ENDPOINT: z + .string() + .optional() + .describe("Public endpoint for accessing S3"), + S3_INTERNAL_ENDPOINT: z + .string() + .optional() + .describe( + "Internal endpoint for accessing S3. This is useful if accessing S3 over public internet is more expensive than via your hosting environment's local network.", + ), + S3_PATH_STYLE: boolString(true).describe( + "Whether the bucket should be accessed using path-style URLs (common for non-AWS providers, eg. '/{bucket}/{key}') or virtual-hosted-style URLs (eg. '{bucket}.s3.amazonaws.com/{key}').", + ), + + /// CloudFront configuration + // Configure these if you'd like to serve assets from the default bucket via CloudFront + // In this case, CAP_AWS_BUCKET_URL should be your CloudFront distribution's URL + CAP_AWS_BUCKET_URL: z + .string() + .optional() + .describe("Public URL of the S3 bucket"), CAP_CLOUDFRONT_DISTRIBUTION_ID: z.string().optional(), - NEXTAUTH_SECRET: z.string(), - NEXTAUTH_URL: z.string(), + CLOUDFRONT_KEYPAIR_ID: z.string().optional(), + CLOUDFRONT_KEYPAIR_PRIVATE_KEY: z.string().optional(), + + /// Google Auth + // Provide these to allow Google login GOOGLE_CLIENT_ID: z.string().optional(), GOOGLE_CLIENT_SECRET: z.string().optional(), + + /// WorkOS SSO + // Provide these to use WorkOS for enterprise SSO WORKOS_CLIENT_ID: z.string().optional(), WORKOS_API_KEY: z.string().optional(), - DUB_API_KEY: z.string().optional(), - RESEND_API_KEY: z.string().optional(), - RESEND_FROM_DOMAIN: z.string().optional(), - DEEPGRAM_API_KEY: z.string().optional(), - NEXT_LOOPS_KEY: z.string().optional(), + + /// Settings + CAP_VIDEOS_DEFAULT_PUBLIC: boolString(true).describe( + "Should videos be public or private by default", + ), + CAP_ALLOWED_SIGNUP_DOMAINS: z + .string() + .optional() + .describe("Comma-separated list of permitted signup domains"), + + /// AI providers + DEEPGRAM_API_KEY: z.string().optional().describe("Audio transcription"), + OPENAI_API_KEY: z.string().optional().describe("AI summaries"), + GROQ_API_KEY: z.string().optional().describe("AI summaries"), + + /// Cap Cloud + // These are only needed for Cap Cloud (https://cap.so) STRIPE_SECRET_KEY: z.string().optional(), STRIPE_WEBHOOK_SECRET: z.string().optional(), DISCORD_FEEDBACK_WEBHOOK_URL: z.string().optional(), DISCORD_LOGS_WEBHOOK_URL: z.string().optional(), - OPENAI_API_KEY: z.string().optional(), - GROQ_API_KEY: z.string().optional(), - INTERCOM_SECRET: z.string().optional(), - CAP_VIDEOS_DEFAULT_PUBLIC: boolString(true), - CAP_ALLOWED_SIGNUP_DOMAINS: z.string().optional(), VERCEL_ENV: z .union([ z.literal("production"), @@ -59,17 +105,20 @@ function createServerEnv() { VERCEL_URL_HOST: z.string().optional(), VERCEL_BRANCH_URL_HOST: z.string().optional(), VERCEL_PROJECT_PRODUCTION_URL_HOST: z.string().optional(), - DOCKER_BUILD: z.string().optional(), - POSTHOG_PERSONAL_API_KEY: z.string().optional(), - CLOUDFRONT_KEYPAIR_ID: z.string().optional(), - CLOUDFRONT_KEYPAIR_PRIVATE_KEY: z.string().optional(), - S3_PUBLIC_ENDPOINT: z.string().optional(), - S3_INTERNAL_ENDPOINT: z.string().optional(), VERCEL_AWS_ROLE_ARN: z.string().optional(), + POSTHOG_PERSONAL_API_KEY: z.string().optional(), + DUB_API_KEY: z.string().optional(), + INTERCOM_SECRET: z.string().optional(), + + /// Ignore + NODE_ENV: z.string(), WORKFLOWS_RPC_URL: z.string().optional(), WORKFLOWS_RPC_SECRET: z.string().optional(), }, experimental__runtimeEnv: { + NODE_ENV: "production", + S3_PUBLIC_ENDPOINT: process.env.CAP_AWS_ENDPOINT, + S3_INTERNAL_ENDPOINT: process.env.CAP_AWS_ENDPOINT, ...process.env, VERCEL_URL_HOST: process.env.VERCEL_URL, VERCEL_BRANCH_URL_HOST: process.env.VERCEL_BRANCH_URL, diff --git a/scripts/env-cli.js b/scripts/env-cli.js index a4b7328606..49c6a71693 100644 --- a/scripts/env-cli.js +++ b/scripts/env-cli.js @@ -92,19 +92,9 @@ async function main() { allEnvs.DATABASE_URL ?? "mysql://root:@localhost:3306/planetscale", }), - DATABASE_MIGRATION_URL: (v) => { - if (v.results.DATABASE_URL?.startsWith("http")) { - log.info("Planetscale HTTP URL detected"); - return text({ - message: "DATABASE_MIGRATION_URL", - }); - } - }, }); envs.DATABASE_URL = dbValues.DATABASE_URL; - if (dbValues.DATABASE_MIGRATION_URL) - envs.DATABASE_MIGRATION_URL = dbValues.DATABASE_MIGRATION_URL; log.info("S3 Envs"); @@ -138,7 +128,6 @@ async function main() { envs = { ...envs, ...s3Values }; } else { envs.DATABASE_URL = DOCKER_DB_ENVS.url; - envs.DATABASE_MIGRATION_URL = DOCKER_DB_ENVS.url; envs.CAP_AWS_ACCESS_KEY = DOCKER_S3_ENVS.accessKey; envs.CAP_AWS_SECRET_KEY = DOCKER_S3_ENVS.secretKey; From 19965084be7f2cabe806481552b16cd49f8060f1 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 4 Nov 2025 11:57:58 +0800 Subject: [PATCH 2/5] add self hosting link --- apps/web/content/docs/self-hosting.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/content/docs/self-hosting.mdx b/apps/web/content/docs/self-hosting.mdx index 8176e3b503..d970251596 100644 --- a/apps/web/content/docs/self-hosting.mdx +++ b/apps/web/content/docs/self-hosting.mdx @@ -64,6 +64,10 @@ To send login links via email, you'll need to configure [Resend](https://resend. 2. Connect a domain and set it as `RESEND_FROM_DOMAIN` 3. Generate an API key and set it as `RESEND_API_KEY` +## Other Configuration + +See [`env/server.ts`](https://github.com/CapSoftware/Cap/blob/main/packages/env/server.ts) for a description of all environment variables you can configure. + ## Support If you encounter a problem with the Docker image or think this documentation is lacking, From ba8beea5ff46671d44b4193e989cb22b3e04f187 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 4 Nov 2025 12:55:19 +0800 Subject: [PATCH 3/5] fix types --- apps/web/instrumentation.node.ts | 2 +- packages/env/server.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/instrumentation.node.ts b/apps/web/instrumentation.node.ts index 4f0e3aaf81..ce13ac04e3 100644 --- a/apps/web/instrumentation.node.ts +++ b/apps/web/instrumentation.node.ts @@ -42,7 +42,7 @@ export async function register() { async function createS3Bucket() { const s3Client = new S3Client({ - endpoint: serverEnv().CAP_AWS_ENDPOINT, + endpoint: serverEnv().S3_INTERNAL_ENDPOINT, region: serverEnv().CAP_AWS_REGION, credentials: { accessKeyId: serverEnv().CAP_AWS_ACCESS_KEY ?? "", diff --git a/packages/env/server.ts b/packages/env/server.ts index 69d71c07f6..8adcc34922 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -116,10 +116,10 @@ function createServerEnv() { WORKFLOWS_RPC_SECRET: z.string().optional(), }, experimental__runtimeEnv: { - NODE_ENV: "production", S3_PUBLIC_ENDPOINT: process.env.CAP_AWS_ENDPOINT, S3_INTERNAL_ENDPOINT: process.env.CAP_AWS_ENDPOINT, ...process.env, + NODE_ENV: process.env.NODE_ENV ?? "production", VERCEL_URL_HOST: process.env.VERCEL_URL, VERCEL_BRANCH_URL_HOST: process.env.VERCEL_BRANCH_URL, VERCEL_PROJECT_PRODUCTION_URL_HOST: From f1679a82d3082ae3ecda0e4b241b925651623be7 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 4 Nov 2025 13:04:17 +0800 Subject: [PATCH 4/5] filter desktop CI --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64148663ce..82dcbd5a9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,8 @@ jobs: id: filter with: filters: | + desktop: + - 'apps/desktop/**' rust: - '.cargo/**' - '.github/**' @@ -154,6 +156,7 @@ jobs: build-desktop: name: Build Desktop + if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop == 'true' strategy: fail-fast: false matrix: From ac7faf120eec75b3e678eafd2ee781467f6c5e9d Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 4 Nov 2025 13:05:21 +0800 Subject: [PATCH 5/5] needs --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82dcbd5a9e..b38b8bf28c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,6 +156,7 @@ jobs: build-desktop: name: Build Desktop + needs: changes if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.desktop == 'true' strategy: fail-fast: false