diff --git a/.env.example b/.env.example index 5dc013c8..a79937c2 100644 --- a/.env.example +++ b/.env.example @@ -22,7 +22,7 @@ BETTER_AUTH_SECRET=replace-with-openssl-rand-base64-32-output # Public URL of the app (used for session cookies and OAuth callbacks) # Local: http://localhost:3000 | Production: https://yourdomain.com -# Railway PR/preview: auto-resolved from RAILWAY_PUBLIC_DOMAIN +# Railway: auto-resolved from RAILWAY_PUBLIC_DOMAIN — only set for custom domains BETTER_AUTH_URL=http://localhost:3000 # Comma-separated allowed origins (optional, for multi-domain setups) diff --git a/server/utils/auth.ts b/server/utils/auth.ts index b37f38c7..708558cf 100644 --- a/server/utils/auth.ts +++ b/server/utils/auth.ts @@ -27,40 +27,24 @@ function resolveTrustedOrigins(baseUrl: string): string[] { function resolveBetterAuthUrl(): string { const explicitUrl = env.BETTER_AUTH_URL?.trim() const railwayDomain = env.RAILWAY_PUBLIC_DOMAIN?.trim() - const hasPreviewDomain = railwayDomain ? railwayDomain.toLowerCase().includes('-pr-') : false - const hasPrNumber = !!env.RAILWAY_GIT_PR_NUMBER?.trim() - const isPreview = isRailwayPreviewEnvironment(env.RAILWAY_ENVIRONMENT_NAME) || hasPreviewDomain || hasPrNumber - - if (!isPreview) { - if (!explicitUrl) { - throw new Error('BETTER_AUTH_URL is required outside Railway PR/preview environments') - } + // Explicit URL always wins (custom domain, local dev, etc.) + if (explicitUrl) { return explicitUrl } + // Derive from Railway's auto-injected public domain (works for all environments) if (railwayDomain) { - const previewUrl = `https://${railwayDomain}` - console.info(`[Reqcore] Using Railway public-domain BETTER_AUTH_URL: ${previewUrl}`) - return previewUrl - } - - const prNumber = env.RAILWAY_GIT_PR_NUMBER?.trim() - if (prNumber) { - console.warn( - `[Reqcore] Railway PR number detected (${prNumber}) but RAILWAY_PUBLIC_DOMAIN is missing. ` + - 'Set BETTER_AUTH_URL explicitly or ensure Railway generated domains are enabled.', - ) - } - - if (explicitUrl) { - console.info('[Reqcore] Using explicit BETTER_AUTH_URL in Railway PR/preview environment') - return explicitUrl + // Railway sets this as bare domain (e.g. "app.up.railway.app"), never with protocol + const domain = railwayDomain.replace(/^https?:\/\//, '') + const url = `https://${domain}` + console.info(`[Reqcore] Using Railway public-domain BETTER_AUTH_URL: ${url}`) + return url } throw new Error( - 'Unable to resolve BETTER_AUTH_URL in Railway PR/preview environment. ' + - 'Set RAILWAY_GIT_PR_NUMBER, RAILWAY_PUBLIC_DOMAIN, or BETTER_AUTH_URL.', + 'BETTER_AUTH_URL is required. Either set it explicitly or generate a public domain in Railway.\n' + + 'Railway users: go to Settings → Networking → Generate Domain, then redeploy.', ) } diff --git a/server/utils/env.ts b/server/utils/env.ts index db64610e..a9b58e2d 100644 --- a/server/utils/env.ts +++ b/server/utils/env.ts @@ -38,7 +38,16 @@ const envSchema = z .object({ DATABASE_URL: z.url(), BETTER_AUTH_SECRET: emptyToUndefined.pipe(z.string().min(32, 'BETTER_AUTH_SECRET must be at least 32 characters')), - BETTER_AUTH_URL: emptyToUndefined.pipe(z.url()).optional(), + BETTER_AUTH_URL: z.preprocess( + (val) => { + if (typeof val !== 'string') return val + const trimmed = val.trim() + // Treat empty strings and broken Railway template refs ("https://") as unset + if (trimmed === '' || trimmed === 'https://' || trimmed === 'http://') return undefined + return trimmed + }, + z.string().url(), + ).optional(), /** Comma-separated list of additional trusted origins for Better Auth CSRF checks. */ BETTER_AUTH_TRUSTED_ORIGINS: emptyToUndefined .pipe(z.string()) @@ -81,17 +90,13 @@ const envSchema = z CRON_SECRET: emptyToUndefined.pipe(z.string().min(16)).optional(), }) .superRefine((data, ctx) => { - const hasPreviewDomain = data.RAILWAY_PUBLIC_DOMAIN - ? data.RAILWAY_PUBLIC_DOMAIN.toLowerCase().includes('-pr-') - : false - const hasPrNumber = !!data.RAILWAY_GIT_PR_NUMBER - const isPreview = isRailwayPreviewEnvironment(data.RAILWAY_ENVIRONMENT_NAME) || hasPreviewDomain || hasPrNumber - - if (!isPreview && !data.BETTER_AUTH_URL) { + // BETTER_AUTH_URL can be derived at runtime from RAILWAY_PUBLIC_DOMAIN, + // so it's only required when not running on Railway. + if (!data.BETTER_AUTH_URL && !data.RAILWAY_PUBLIC_DOMAIN) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['BETTER_AUTH_URL'], - message: 'BETTER_AUTH_URL is required outside Railway PR/preview environments', + message: 'BETTER_AUTH_URL is required when RAILWAY_PUBLIC_DOMAIN is not available', }) } }) @@ -124,7 +129,7 @@ export const env = new Proxy({} as z.infer, { `\n[Reqcore] ❌ Missing or invalid environment variables:\n${missing}\n\n` + `Ensure these variables are set in your Railway service (Settings → Variables).\n` + `Required: DATABASE_URL, BETTER_AUTH_SECRET, S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET\n` + - `Required outside Railway PR/preview environments: BETTER_AUTH_URL\n` + + `Required when not on Railway: BETTER_AUTH_URL (or generate a Railway domain)\n` + `Optional: BETTER_AUTH_TRUSTED_ORIGINS, S3_REGION (default: us-east-1), S3_FORCE_PATH_STYLE (default: true), TRUSTED_PROXY_IP, DEMO_ORG_SLUG, RESEND_API_KEY, RESEND_FROM_EMAIL\n`, ) throw result.error