diff --git a/.env.example b/.env.example
index c4c2acd01..3a02a8b96 100644
--- a/.env.example
+++ b/.env.example
@@ -1,24 +1,64 @@
-# Uncomment according to src/lib/env.ts
+# =================================
+# REQUIRED SERVER ENVIRONMENT VARIABLES
+# =================================
+# Supabase service role key for admin operations
+SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
-# KV_URL=
-# KV_REST_API_READ_ONLY_TOKEN=
-# KV_REST_API_TOKEN=
-# KV_REST_API_URL=
-# SUPABASE_SERVICE_ROLE_KEY=
-# COOKIE_ENCRYPTION_KEY=
+# Key for encrypting cookies - must be at least 32 characters long
+COOKIE_ENCRYPTION_KEY=your_secure_cookie_encryption_key_here
-BILLING_API_URL=https://billing.e2b.dev
+# KV database configuration
+KV_URL=
+KV_REST_API_READ_ONLY_TOKEN=
+KV_REST_API_TOKEN=
+KV_REST_API_URL=
-# NEXT_PUBLIC_POSTHOG_KEY=
-# NEXT_PUBLIC_SUPABASE_URL=
-# NEXT_PUBLIC_SUPABASE_ANON_KEY=
-# NEXT_PUBLIC_STRIPE_BILLING_URL=
+# =================================
+# REQUIRED CLIENT ENVIRONMENT VARIABLES
+# =================================
+# Supabase URL and anon key for client-side operations
+NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
+
+# API domain for your application
NEXT_PUBLIC_DEFAULT_API_DOMAIN=e2b.dev
-# Set to 1 to expose
-NEXT_PUBLIC_EXPOSE_STORYBOOK=0
-NEXT_PUBLIC_SCAN=0
-NEXT_PUBLIC_MOCK_DATA=0
+# =================================
+# OPTIONAL SERVER ENVIRONMENT VARIABLES
+# =================================
+# Billing API URL (Required if NEXT_PUBLIC_INCLUDE_BILLING=1)
+# BILLING_API_URL=https://billing.e2b.dev
+
+# Vercel URL (automatically set in Vercel deployments)
+# VERCEL_URL=
+
+# Development infrastructure API domain
+# DEVELOPMENT_INFRA_API_DOMAIN=
+
+# Sentry authentication token for error reporting
+# SENTRY_AUTH_TOKEN=
+
+# ZeroBounce API key for email validation
+# ZEROBOUNCE_API_KEY=
+
+# =================================
+# OPTIONAL CLIENT ENVIRONMENT VARIABLES
+# =================================
+# PostHog analytics key
+# NEXT_PUBLIC_POSTHOG_KEY=
+
+# Enable billing features: set to 1 to enable
+# When enabled, both BILLING_API_URL and NEXT_PUBLIC_STRIPE_BILLING_URL must be provided
+# NEXT_PUBLIC_INCLUDE_BILLING=0
+
+# Stripe billing URL (Required if NEXT_PUBLIC_INCLUDE_BILLING=1)
+# NEXT_PUBLIC_STRIPE_BILLING_URL=https://billing.stripe.com/your-path
+
+# Set to 1 to expose Storybook
+# NEXT_PUBLIC_EXPOSE_STORYBOOK=0
+
+# Set to 1 to enable scanning
+# NEXT_PUBLIC_SCAN=0
-# For applying migrations
-# POSTGRES_CONNECTION_STRING=
+# Set to 1 to use mock data
+# NEXT_PUBLIC_MOCK_DATA=0
\ No newline at end of file
diff --git a/package.json b/package.json
index 00d0545ab..ae6f7491f 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"<<<<<< Development": "",
"storybook": "storybook dev -p 6006",
"shad": "bunx shadcn@canary",
- "prebuild": "bun scripts:build-storybook",
+ "prebuild": "bun scripts:check-app-env && bun scripts:build-storybook",
"postinstall": "fumadocs-mdx",
"<<<<<< Testing": "",
"test:run": "bun scripts:check-all-env && vitest run",
diff --git a/scripts/check-app-env.ts b/scripts/check-app-env.ts
index 45f7dbc4c..68bf2683f 100644
--- a/scripts/check-app-env.ts
+++ b/scripts/check-app-env.ts
@@ -4,4 +4,19 @@ import { loadEnvConfig } from '@next/env'
const projectDir = process.cwd()
loadEnvConfig(projectDir)
-validateEnv(serverSchema.merge(clientSchema))
+const schema = serverSchema.merge(clientSchema).refine(
+ (data) => {
+ if (data.NEXT_PUBLIC_INCLUDE_BILLING === '1') {
+ return !!data.BILLING_API_URL && !!data.NEXT_PUBLIC_STRIPE_BILLING_URL
+ }
+
+ return true
+ },
+ {
+ message:
+ 'NEXT_PUBLIC_INCLUDE_BILLING is enabled, but either BILLING_API_URL or NEXT_PUBLIC_STRIPE_BILLING_URL is missing',
+ path: ['BILLING_API_URL', 'NEXT_PUBLIC_STRIPE_BILLING_URL'],
+ }
+)
+
+validateEnv(schema)
diff --git a/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx b/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx
index c0cc74f0b..699c263b5 100644
--- a/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx
+++ b/src/app/dashboard/[teamIdOrSlug]/usage/page.tsx
@@ -30,13 +30,7 @@ async function UsagePageContent({ teamId }: { teamId: string }) {
const res = await getUsage({ teamId })
if (!res?.data || res.serverError || res.validationErrors) {
- return (
-
- )
+ throw new Error(res?.serverError || 'Failed to load usage')
}
const data = res.data
diff --git a/src/app/dashboard/error.tsx b/src/app/dashboard/error.tsx
index bc029c3e7..b107605a6 100644
--- a/src/app/dashboard/error.tsx
+++ b/src/app/dashboard/error.tsx
@@ -1,12 +1,15 @@
'use client'
-import { UnknownError } from '@/types/errors'
import ErrorBoundary from '@/ui/error'
-export default function DashboardError() {
+export default function DashboardError({
+ error,
+}: {
+ error: Error & { digest?: string }
+}) {
return (
diff --git a/src/app/error.tsx b/src/app/error.tsx
index aa9898f76..3e14002a4 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -1,8 +1,16 @@
'use client'
-import { createErrorBoundary } from '@/lib/create-error-boundary'
+import ErrorBoundary from '@/ui/error'
-export default createErrorBoundary({
- title: 'Something went wrong',
- description: 'An unexpected error occurred',
-})
+export default function Error({
+ error,
+}: {
+ error: Error & { digest?: string }
+}) {
+ return (
+
+ )
+}
diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx
index 23a4a9fd8..0f7cb255c 100644
--- a/src/app/global-error.tsx
+++ b/src/app/global-error.tsx
@@ -1,6 +1,6 @@
'use client'
-import { SentryErrorBoundary } from '@/ui/sentry-error-boundary'
+import ErrorBoundary from '@/ui/error'
export default function GlobalError({
error,
@@ -8,11 +8,9 @@ export default function GlobalError({
error: Error & { digest?: string }
}) {
return (
-
)
}
diff --git a/src/configs/dashboard-navs.ts b/src/configs/dashboard-navs.ts
index 9fbee083b..6f91732c7 100644
--- a/src/configs/dashboard-navs.ts
+++ b/src/configs/dashboard-navs.ts
@@ -9,6 +9,7 @@ import {
Users,
} from 'lucide-react'
import { ForwardRefExoticComponent, RefAttributes } from 'react'
+import { INCLUDE_BILLING } from './flags'
type DashboardNavLinkArgs = {
teamIdOrSlug?: string
@@ -35,11 +36,16 @@ export const MAIN_DASHBOARD_LINKS: DashboardNavLink[] = [
href: (args) => `/dashboard/${args.teamIdOrSlug}/templates`,
icon: Container,
},
- {
- label: 'Usage',
- href: (args) => `/dashboard/${args.teamIdOrSlug}/usage`,
- icon: Activity,
- },
+ ...(INCLUDE_BILLING
+ ? [
+ {
+ label: 'Usage',
+ href: (args: DashboardNavLinkArgs) =>
+ `/dashboard/${args.teamIdOrSlug}/usage`,
+ icon: Activity,
+ },
+ ]
+ : []),
{
label: 'Team',
@@ -53,16 +59,23 @@ export const MAIN_DASHBOARD_LINKS: DashboardNavLink[] = [
icon: Key,
group: 'manage',
},
- {
- label: 'Billing',
- href: (args) => `/dashboard/${args.teamIdOrSlug}/billing`,
- icon: CreditCard,
- group: 'expenses',
- },
- {
- label: 'Budget',
- href: (args) => `/dashboard/${args.teamIdOrSlug}/budget`,
- group: 'expenses',
- icon: DollarSign,
- },
+
+ ...(INCLUDE_BILLING
+ ? [
+ {
+ label: 'Billing',
+ href: (args: DashboardNavLinkArgs) =>
+ `/dashboard/${args.teamIdOrSlug}/billing`,
+ icon: CreditCard,
+ group: 'expenses',
+ },
+ {
+ label: 'Budget',
+ href: (args: DashboardNavLinkArgs) =>
+ `/dashboard/${args.teamIdOrSlug}/budget`,
+ group: 'expenses',
+ icon: DollarSign,
+ },
+ ]
+ : []),
]
diff --git a/src/configs/flags.ts b/src/configs/flags.ts
new file mode 100644
index 000000000..a4728e522
--- /dev/null
+++ b/src/configs/flags.ts
@@ -0,0 +1 @@
+export const INCLUDE_BILLING = process.env.NEXT_PUBLIC_INCLUDE_BILLING === '1'
diff --git a/src/features/client-providers.tsx b/src/features/client-providers.tsx
index 6bbc63cdf..4cbbe5eb7 100644
--- a/src/features/client-providers.tsx
+++ b/src/features/client-providers.tsx
@@ -32,6 +32,10 @@ export default function ClientProviders({ children }: ClientProvidersProps) {
export function PostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
+ if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) {
+ return
+ }
+
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
// Note that PostHog will automatically capture page views and common events
//
diff --git a/src/features/dashboard/billing/credits-content.tsx b/src/features/dashboard/billing/credits-content.tsx
index 93ac20eb5..4fc4ffc23 100644
--- a/src/features/dashboard/billing/credits-content.tsx
+++ b/src/features/dashboard/billing/credits-content.tsx
@@ -1,5 +1,4 @@
import { getUsage } from '@/server/usage/get-usage'
-import { ErrorIndicator } from '@/ui/error-indicator'
export default async function BillingCreditsContent({
teamId,
@@ -9,15 +8,7 @@ export default async function BillingCreditsContent({
const res = await getUsage({ teamId })
if (!res?.data || res.serverError) {
- return (
-
-
-
- )
+ throw new Error(res?.serverError || 'Failed to load credits')
}
const usage = res.data
diff --git a/src/features/dashboard/budget/usage-limits.tsx b/src/features/dashboard/budget/usage-limits.tsx
index b3d4377a4..256caabf4 100644
--- a/src/features/dashboard/budget/usage-limits.tsx
+++ b/src/features/dashboard/budget/usage-limits.tsx
@@ -2,8 +2,6 @@ import { getBillingLimits } from '@/server/billing/get-billing-limits'
import LimitCard from './limit-card'
import AlertCard from './alert-card'
import { cn } from '@/lib/utils'
-import Dotted from '@/ui/dotted'
-import { ErrorIndicator } from '@/ui/error-indicator'
interface UsageLimitsProps {
className?: string
@@ -17,15 +15,7 @@ export default async function UsageLimits({
const res = await getBillingLimits({ teamId })
if (!res?.data || res.serverError || res.validationErrors) {
- return (
-
-
-
- )
+ throw new Error(res?.serverError || 'Failed to load usage limits')
}
const limits = res.data
diff --git a/src/features/dashboard/page-layout.tsx b/src/features/dashboard/page-layout.tsx
index 1c21ae468..f801eb819 100644
--- a/src/features/dashboard/page-layout.tsx
+++ b/src/features/dashboard/page-layout.tsx
@@ -4,6 +4,9 @@ import { Suspense } from 'react'
import SidebarMobile from './sidebar/sidebar-mobile'
import Frame from '@/ui/frame'
import { DashboardSurveyPopover } from './navbar/dashboard-survey-popover'
+import { ErrorBoundary } from 'react-error-boundary'
+import E2BErrorBoundary, { CatchErrorBoundary } from '@/ui/error'
+import { UnknownError } from '@/types/errors'
interface DashboardPageLayoutProps {
children: React.ReactNode
@@ -43,14 +46,16 @@ export default async function DashboardPageLayout({
-
- {children}
-
- {children}
+
+
+ {children}
+
+ {children}
+
)
}
diff --git a/src/lib/create-error-boundary.tsx b/src/lib/create-error-boundary.tsx
deleted file mode 100644
index 20f12eee8..000000000
--- a/src/lib/create-error-boundary.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client'
-
-import { SentryErrorBoundary } from '@/ui/sentry-error-boundary'
-
-interface CreateErrorBoundaryOptions {
- title?: string
- description?: string
-}
-
-/**
- * Helper function to create error.tsx files for any route in the application
- *
- * Usage:
- * ```
- * // src/app/some-route/error.tsx
- * export default createErrorBoundary({
- * title: 'Route Error',
- * description: 'Something went wrong in this route'
- * })
- * ```
- */
-export function createErrorBoundary(options: CreateErrorBoundaryOptions = {}) {
- return function ErrorBoundary({
- error,
- reset,
- }: {
- error: Error & { digest?: string }
- reset: () => void
- }) {
- return (
-
- )
- }
-}
diff --git a/src/lib/env.ts b/src/lib/env.ts
index 8e3a5c40e..78ce1eeb2 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -1,10 +1,10 @@
import { z } from 'zod'
export const serverSchema = z.object({
- SUPABASE_SERVICE_ROLE_KEY: z.string(),
- BILLING_API_URL: z.string().url(),
- COOKIE_ENCRYPTION_KEY: z.string(),
+ SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
+ COOKIE_ENCRYPTION_KEY: z.string().min(32),
+ BILLING_API_URL: z.string().url().optional(),
VERCEL_URL: z.string().optional(),
DEVELOPMENT_INFRA_API_DOMAIN: z.string().optional(),
SENTRY_AUTH_TOKEN: z.string().optional(),
@@ -12,11 +12,13 @@ export const serverSchema = z.object({
})
export const clientSchema = z.object({
- NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1),
NEXT_PUBLIC_SUPABASE_URL: z.string().url(),
NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1),
NEXT_PUBLIC_DEFAULT_API_DOMAIN: z.string(),
- NEXT_PUBLIC_STRIPE_BILLING_URL: z.string().url(),
+
+ NEXT_PUBLIC_POSTHOG_KEY: z.string().min(1).optional(),
+ NEXT_PUBLIC_INCLUDE_BILLING: z.string().optional(),
+ NEXT_PUBLIC_STRIPE_BILLING_URL: z.string().url().optional(),
NEXT_PUBLIC_EXPOSE_STORYBOOK: z.string().optional(),
NEXT_PUBLIC_SCAN: z.string().optional(),
NEXT_PUBLIC_MOCK_DATA: z.string().optional(),
@@ -31,13 +33,10 @@ export const testEnvSchema = z.object({
* You can't destruct `process.env` as a regular object, so we do
* a simple validation of the environment variables we need.
*/
-export const formatErrors = (
- errors: z.ZodFormattedError