import Link from "next/link"; import { ArrowRight, Check, KeyRound, ShieldCheck } from "lucide-react"; import { PLAN_LIMITS, isUnlimited, normalizePlan } from "@fetcha/core"; import { getWorkspace } from "@/lib/session"; import { monthlyUsage } from "@/lib/queries/account"; import { formatBytes, formatNumber, formatUsd, formatDateOnly } from "@/lib/format"; import { PageHeader } from "@/components/ui/page-header"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Alert } from "@/components/ui/alert"; export const dynamic = "force-dynamic"; export const metadata = { title: "Plan & access · Fetcha" }; function seconds(ms: number) { return `${ms / 1000} s`; } export default async function BillingPage() { const ws = await getWorkspace(); const limits = PLAN_LIMITS[normalizePlan(ws.organization.plan)]; const unlimited = isUnlimited(limits); const now = new Date(); const monthStart = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1)); const usage = await monthlyUsage(ws.organization.id); const included: Array<{ label: string; detail?: string }> = [ { label: unlimited ? "Unlimited requests" : `${formatNumber(limits.monthly_requests)} requests / month`, detail: "no monthly quota, no overage" }, { label: `${formatNumber(limits.concurrency)} concurrent requests`, detail: "per organization, all projects together" }, { label: `${seconds(limits.max_timeout_ms)} maximum timeout`, detail: `up to ${limits.max_retries} automatic retries per request` }, { label: "All network classes", detail: limits.networks.map((n) => n[0]!.toUpperCase() + n.slice(1)).join(", ") }, { label: "Managed browser rendering", detail: `${limits.browser_concurrency} concurrent renders, routed through the same networks` }, { label: `Crawl jobs up to ${formatNumber(limits.crawl_max_pages)} pages`, detail: `${limits.crawl_concurrent_jobs} jobs in parallel, sitemap discovery and Markdown output` }, { label: "Sticky sessions and geo targeting", detail: "country, region and city on every request" }, { label: `${limits.retention_days}-day request logs`, detail: "full metadata, attempts and timings" }, ]; return (
Unlimited — private platform {limits.label}
Every organization on this platform gets the same limits. They are enforced by the API and shown here from the same configuration the API uses.
    {included.map((line) => (
  • {line.label} {line.detail ? {line.detail} : null}
  • ))}
Usage this month Across all projects of {ws.organization.name}, since {formatDateOnly(monthStart)}. Resets {formatDateOnly(nextMonth)}.
Requests {formatNumber(usage.requests)} / ∞
Successful {formatNumber(usage.successful)}
Bandwidth {formatBytes(usage.bytes)}
Metered spend {formatUsd(usage.spendUsd)}

Spend is an internal cost estimate used only for your optional spending limits. Nothing is invoiced.

Detailed usage
Settings } > Optional guard-rails, not billing: cap the estimated monthly spend for the whole organization{ws.organization.hardLimitUsd ? ` (currently ${formatUsd(ws.organization.hardLimitUsd)} hard limit)` : ""} or per project. Soft limits email you; hard limits stop requests with USAGE_LIMIT_REACHED. Manage access ) : ( ) } > Accounts are created by invitation. To bring a teammate in, ask your Fetcha administrator to add their email to the access list; they will receive an invitation to sign up.
); }