Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 10 additions & 26 deletions server/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
)
}

Expand Down
25 changes: 15 additions & 10 deletions server/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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',
})
}
})
Expand Down Expand Up @@ -124,7 +129,7 @@ export const env = new Proxy({} as z.infer<typeof envSchema>, {
`\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
Expand Down
Loading