pr_01m47d24b0e6n91zwymwxg0vpx/apps/web/app/routes/pricing.tsx
| 1 | import { ArrowUpRight } from "lucide-react"; |
| 2 | |
| 3 | import { DEPLOYMENTS_ALLOWANCE, type FeaturePlan, type FreeTier, MICROS_PER_DOLLAR, type Price } from "@g1t/contracts"; |
| 4 | |
| 5 | import type { Route } from "./+types/pricing"; |
| 6 | import { wholeDollars } from "../lib/billing"; |
| 7 | import { page } from "../lib/meta"; |
| 8 | import { TimeAgo } from "../components/ui"; |
| 9 | import { billing } from "../lib/services.server"; |
| 10 | |
| 11 | export function meta(args: Route.MetaArgs) { |
| 12 | return page(args, { |
| 13 | title: "Pricing · g1t", |
| 14 | description: |
| 15 | "The forge is free. One plan, $20 a month per workspace with $10 of usage included; usage at what it costs g1t plus 20%. No seats, ever.", |
| 16 | }); |
| 17 | } |
| 18 | |
| 19 | export async function loader() { |
| 20 | const [book, status] = await Promise.all([billing.prices().catch(() => null), billing.status().catch(() => null)]); |
| 21 | return { book, free: status?.free ?? false }; |
| 22 | } |
| 23 | |
| 24 | /** A price in dollars, with as many digits as it needs to say anything. */ |
| 25 | function money(micros: number): string { |
| 26 | const dollars = micros / MICROS_PER_DOLLAR; |
| 27 | if (dollars >= 1) return `$${dollars.toFixed(2)}`; |
| 28 | if (dollars >= 0.01) return `$${dollars.toFixed(3)}`; |
| 29 | return `$${dollars.toPrecision(2)}`; |
| 30 | } |
| 31 | |
| 32 | /** Per second is easier to read per minute. */ |
| 33 | function perUnit(price: Price, micros: number): string { |
| 34 | return price.unit === "second" ? `${money(micros * 60)} a minute` : `${money(micros)} per ${price.unit}`; |
| 35 | } |
| 36 | |
| 37 | function gigabytes(bytes: number): string { |
| 38 | return `${Math.round((bytes / 1e9) * 10) / 10} GB`; |
| 39 | } |
| 40 | |
| 41 | function count(n: number): string { |
| 42 | return n.toLocaleString("en-US"); |
| 43 | } |
| 44 | |
| 45 | /** What the page says when billing cannot be reached: the published defaults. */ |
| 46 | const DEFAULT_FREE: Required<FreeTier> = { |
| 47 | trialWorkspaceMicros: 5_000_000, |
| 48 | trialMonthlyPoolMicros: 100_000_000, |
| 49 | ossPoolMicros: 25_000_000, |
| 50 | ossRepoMicros: 2_000_000, |
| 51 | freePrivateStorageBytes: 1_000_000_000, |
| 52 | auditRetentionDays: 90, |
| 53 | minChargeMicros: 5_000_000, |
| 54 | gitOperationsIncluded: 10_000, |
| 55 | gitOperationsFreeCap: 50_000, |
| 56 | planPrivateStorageBytes: 10_000_000_000, |
| 57 | paidStartCeilingMicros: 100_000_000, |
| 58 | overageForgiveCostMicros: 50_000_000, |
| 59 | }; |
| 60 | |
| 61 | const A = DEPLOYMENTS_ALLOWANCE; |
| 62 | const DEPLOYMENTS_LINE = `${A.apps} apps, ${A.requests / 1e6} million requests, ${A.cpuMs / 1e6} million CPU milliseconds, ${A.customDomains} custom domains and ${A.buildSeconds / 60} build minutes a month`; |
| 63 | |
| 64 | const DEFAULT_PLAN: FeaturePlan = { |
| 65 | feature: "plan", |
| 66 | title: "g1t", |
| 67 | monthlyCents: 2000, |
| 68 | includes: [ |
| 69 | "$10 of usage each month at cost plus 20%, used first. Unused included usage does not roll over.", |
| 70 | "Agents, checks, workflows, the merge queue, deployments and semantic search", |
| 71 | `Deployments: ${DEPLOYMENTS_LINE}`, |
| 72 | "10 GB of private repository storage", |
| 73 | "Unlimited members, at one price for the workspace: never per person", |
| 74 | ], |
| 75 | overage: "Usage past what is included is charged at cost plus 20%.", |
| 76 | }; |
| 77 | |
| 78 | /** The ways to reach g1t about an enterprise account. */ |
| 79 | const ENTERPRISE_MAIL = "mailto:hey@flagon.io?subject=Enterprise%20billing%20for%20g1t"; |
| 80 | |
| 81 | const HOW = [ |
| 82 | { |
| 83 | title: "Our cost, passed through", |
| 84 | body: "Every sandbox second, build, app request and model token costs g1t money at Cloudflare or a model provider. Each is metered and charged at that cost plus 20%, from the first second and the first request. Use a little, pay a little.", |
| 85 | }, |
| 86 | { |
| 87 | title: "Prices follow costs, by themselves", |
| 88 | body: "Model runs are charged at what Cloudflare's AI Gateway priced each request at, so a provider's price change reaches you the same day. Every day, each Cloudflare cost is checked against what Cloudflare billed g1t; when one moves, its price moves with it, and the change is listed below.", |
| 89 | }, |
| 90 | { |
| 91 | title: "The 20% is the overhead", |
| 92 | body: "It pays for running g1t and for building and keeping up the features you use. The same 20% on everything, and nothing bundled in.", |
| 93 | }, |
| 94 | { |
| 95 | title: "No seats, ever", |
| 96 | body: "Add as many people and agents as you like. A workspace pays one flat price for the plan, and for what it uses past what the plan includes.", |
| 97 | }, |
| 98 | ]; |
| 99 | |
| 100 | /** One row of the free-and-plan table. */ |
| 101 | type Row = { what: string; free: string; plan: string; note?: string }; |
| 102 | |
| 103 | function rows(tier: Required<FreeTier>): Row[] { |
| 104 | return [ |
| 105 | { |
| 106 | what: "The forge", |
| 107 | free: "Free", |
| 108 | plan: "Included", |
| 109 | note: "Public and private repositories, issues, pull requests, reviews, protected branches, code owners, search and Explore.", |
| 110 | }, |
| 111 | { |
| 112 | what: "Agents, checks, workflows, merge queue", |
| 113 | free: "After a card check: the trial, and the open-source pool for checks, workflows and the merge queue on public repositories", |
| 114 | plan: "From the plan's included usage, then at cost plus 20%", |
| 115 | }, |
| 116 | { |
| 117 | what: "Deployments", |
| 118 | free: "Not available", |
| 119 | plan: DEPLOYMENTS_LINE.replace(/^./, (c) => c.toUpperCase()) + ", then at cost plus 20%", |
| 120 | }, |
| 121 | { |
| 122 | what: "Semantic search", |
| 123 | free: "After a card check, from the trial", |
| 124 | plan: "From the plan's included usage, then at cost plus 20%", |
| 125 | }, |
| 126 | { |
| 127 | what: "Private storage", |
| 128 | free: `${gigabytes(tier.freePrivateStorageBytes)}, never charged. Past it, pushes to private repositories stop until you make room or start the plan.`, |
| 129 | plan: `${gigabytes(tier.planPrivateStorageBytes)}, then at cost plus 20%`, |
| 130 | note: "Public repositories are never charged for storage.", |
| 131 | }, |
| 132 | { |
| 133 | what: "Git operations", |
| 134 | free: `${count(tier.gitOperationsIncluded)} a month, and never charged. Past ${count(tier.gitOperationsFreeCap)} in a month, slowed to 60 an hour until the month turns.`, |
| 135 | plan: `${count(tier.gitOperationsIncluded)} a month, then at cost plus 20%`, |
| 136 | note: "Clones, fetches and pushes through g1t.", |
| 137 | }, |
| 138 | { |
| 139 | what: "Audit log", |
| 140 | free: `${tier.auditRetentionDays} days, with export`, |
| 141 | plan: `${tier.auditRetentionDays} days, with export`, |
| 142 | }, |
| 143 | { what: "Secret push protection", free: "Included", plan: "Included" }, |
| 144 | { what: "Single sign-on", free: "On every plan, once it is built", plan: "On every plan, once it is built" }, |
| 145 | { what: "Members", free: "Unlimited", plan: "Unlimited" }, |
| 146 | { |
| 147 | what: "Usage past what is included", |
| 148 | free: "Not possible: a free workspace never runs up a bill", |
| 149 | plan: "Opt in, up to a spend limit you set", |
| 150 | }, |
| 151 | ]; |
| 152 | } |
| 153 | |
| 154 | export default function Pricing({ loaderData }: Route.ComponentProps) { |
| 155 | const { book, free } = loaderData; |
| 156 | const checked = book?.prices.map((p) => p.checkedAt).filter((at): at is string => !!at).sort().at(-1); |
| 157 | const given = Object.fromEntries(Object.entries(book?.free ?? {}).filter(([, value]) => value != null)); |
| 158 | const tier: Required<FreeTier> = { ...DEFAULT_FREE, ...given }; |
| 159 | const plan = book?.plans?.find((p) => p.feature === "plan") ?? DEFAULT_PLAN; |
| 160 | const price = plan.monthlyCents / 100; |
| 161 | const dollars = wholeDollars; |
| 162 | return ( |
| 163 | <main className="mx-auto max-w-4xl px-4 py-12"> |
| 164 | <p className="text-sm font-medium text-accent">Pricing</p> |
| 165 | <h1 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">One plan, and usage at cost plus 20%</h1> |
| 166 | <p className="mt-3 max-w-2xl text-muted"> |
| 167 | The forge is free for everyone. Work that runs on g1t's machines is metered at what it costs g1t, plus 20%. The |
| 168 | numbers on this page are the live price book g1t charges from. |
| 169 | </p> |
| 170 | {free && ( |
| 171 | <div className="mt-5 rounded-xl border border-accent/30 bg-accent/5 px-4 py-3 text-sm"> |
| 172 | <span className="font-medium">Free while g1t is being built out.</span>{" "} |
| 173 | <span className="text-muted">Usage is recorded at these prices but not charged for now. The plan is charged.</span> |
| 174 | </div> |
| 175 | )} |
| 176 | |
| 177 | <section className="mt-10 rounded-xl border border-accent/40 bg-surface p-6"> |
| 178 | <div className="flex flex-wrap items-baseline justify-between gap-3"> |
| 179 | <h2 className="text-xl font-semibold tracking-tight">{plan.title}</h2> |
| 180 | <p> |
| 181 | <span className="text-3xl font-semibold tabular-nums">${price}</span>{" "} |
| 182 | <span className="text-sm text-muted">a month per workspace</span> |
| 183 | </p> |
| 184 | </div> |
| 185 | <ul className="mt-4 space-y-1.5 text-sm text-muted"> |
| 186 | {plan.includes.map((line) => ( |
| 187 | <li key={line} className="flex gap-2"> |
| 188 | <span aria-hidden className="text-accent"> |
| 189 | ✓ |
| 190 | </span> |
| 191 | {line} |
| 192 | </li> |
| 193 | ))} |
| 194 | </ul> |
| 195 | <p className="mt-3 text-xs text-faint">{plan.overage}</p> |
| 196 | <div className="mt-5 grid gap-5 border-t border-line pt-5 text-sm sm:grid-cols-2"> |
| 197 | <div> |
| 198 | <p className="font-medium">Where the ${price} goes</p> |
| 199 | <p className="mt-1 text-muted"> |
| 200 | $10 comes back to you as usage: agents, sandboxes, builds and deployments at cost plus 20%, used before |
| 201 | anything else. The other $10 pays for running g1t, the free forge for everyone, and the people building it. |
| 202 | </p> |
| 203 | </div> |
| 204 | <div> |
| 205 | <p className="font-medium">Included usage resets each month</p> |
| 206 | <p className="mt-1 text-muted"> |
| 207 | Unused included usage does not roll over: it starts again at $10 when the month turns. Past it, usage is |
| 208 | charged at cost plus 20%. |
| 209 | </p> |
| 210 | </div> |
| 211 | </div> |
| 212 | <p className="mt-5 text-sm text-muted">An owner starts the plan from the workspace's Billing page.</p> |
| 213 | </section> |
| 214 | |
| 215 | <h2 className="mt-14 text-xl font-semibold tracking-tight">Free and the plan, side by side</h2> |
| 216 | <p className="mt-1 text-sm text-muted">No security feature is held back for a plan.</p> |
| 217 | {/* On a phone, one row at a time with both answers under it; a table that wide would scroll. */} |
| 218 | <ul className="mt-4 divide-y divide-line rounded-xl border border-line text-sm sm:hidden"> |
| 219 | {rows(tier).map((row) => ( |
| 220 | <li key={row.what} className="px-4 py-3"> |
| 221 | <p className="font-medium">{row.what}</p> |
| 222 | {row.note && <p className="text-xs text-faint">{row.note}</p>} |
| 223 | <dl className="mt-2 grid grid-cols-[4.5rem_1fr] gap-x-3 gap-y-1.5"> |
| 224 | <dt className="text-xs leading-5 text-faint">Free</dt> |
| 225 | <dd className="text-muted">{row.free}</dd> |
| 226 | <dt className="text-xs leading-5 text-faint">{plan.title}</dt> |
| 227 | <dd className="text-muted">{row.plan}</dd> |
| 228 | </dl> |
| 229 | </li> |
| 230 | ))} |
| 231 | </ul> |
| 232 | <div className="mt-4 hidden overflow-x-auto rounded-xl border border-line sm:block"> |
| 233 | <table className="w-full min-w-[36rem] text-left text-sm"> |
| 234 | <thead className="border-b border-line text-xs text-muted"> |
| 235 | <tr> |
| 236 | <th className="px-4 py-2.5 font-medium"> |
| 237 | <span className="sr-only">What</span> |
| 238 | </th> |
| 239 | <th className="px-4 py-2.5 font-medium">Free</th> |
| 240 | <th className="px-4 py-2.5 font-medium"> |
| 241 | {plan.title}, ${price} a month |
| 242 | </th> |
| 243 | </tr> |
| 244 | </thead> |
| 245 | <tbody className="divide-y divide-line"> |
| 246 | {rows(tier).map((row) => ( |
| 247 | <tr key={row.what} className="align-top"> |
| 248 | <td className="px-4 py-3"> |
| 249 | <p className="font-medium">{row.what}</p> |
| 250 | {row.note && <p className="text-xs text-faint">{row.note}</p>} |
| 251 | </td> |
| 252 | <td className="px-4 py-3 text-muted">{row.free}</td> |
| 253 | <td className="px-4 py-3 text-muted">{row.plan}</td> |
| 254 | </tr> |
| 255 | ))} |
| 256 | </tbody> |
| 257 | </table> |
| 258 | </div> |
| 259 | <p className="mt-3 text-sm text-muted"> |
| 260 | Git operations cost g1t money too: Cloudflare charges g1t $0.15 per 1,000. {count(tier.gitOperationsIncluded)} a |
| 261 | month is about twenty times what an active workspace uses; an agent run takes two to four. |
| 262 | </p> |
| 263 | |
| 264 | <h2 className="mt-14 text-xl font-semibold tracking-tight">No card, no compute</h2> |
| 265 | <div className="mt-3 max-w-3xl space-y-3 text-sm text-muted"> |
| 266 | <p> |
| 267 | Agents, checks, workflows, the merge queue, deployments and semantic search run on g1t's machines. They need |
| 268 | the plan, or a card check. |
| 269 | </p> |
| 270 | <p> |
| 271 | The reason: compute costs g1t real money from the first second, and free compute draws people who use it to |
| 272 | mine cryptocurrency. A card check is the smallest gate that stops that, with one trial per card. |
| 273 | </p> |
| 274 | <p> |
| 275 | A card check is never charged. Stripe saves and verifies the card, with 3-D Secure where your bank asks for it. |
| 276 | Nothing is charged until an owner starts the plan, and then the same card is used, without asking for it again. |
| 277 | The trial needs a credit or debit card; prepaid cards can still pay for the plan. |
| 278 | </p> |
| 279 | </div> |
| 280 | |
| 281 | <div className="mt-5 grid gap-4 sm:grid-cols-2"> |
| 282 | <section className="rounded-xl border border-line bg-surface p-5"> |
| 283 | <h3 className="font-medium">The trial</h3> |
| 284 | <p className="mt-1.5 text-sm text-muted"> |
| 285 | {dollars(tier.trialWorkspaceMicros)} of usage, once per workspace and once per card, after a card check with a credit or |
| 286 | debit card. It comes from a{" "} |
| 287 | {dollars(tier.trialMonthlyPoolMicros)} pool each month for everyone; when a month's pool is given out, new |
| 288 | trials wait until the 1st. The trial does not cover deployments and never turns into a charge. If the last |
| 289 | run on a trial goes past it, g1t covers the difference. |
| 290 | </p> |
| 291 | </section> |
| 292 | <section className="rounded-xl border border-line bg-surface p-5"> |
| 293 | <h3 className="font-medium">The open-source pool</h3> |
| 294 | <p className="mt-1.5 text-sm text-muted"> |
| 295 | Checks, workflows and the merge queue on public repositories, after a card check:{" "} |
| 296 | {dollars(tier.ossPoolMicros)} a month in all, up to {dollars(tier.ossRepoMicros)} a month per repository. |
| 297 | Agents do not run from the pool. When the pool or a repository's share is used up, the work waits for the |
| 298 | month to turn, or the workspace pays as usual. |
| 299 | </p> |
| 300 | </section> |
| 301 | </div> |
| 302 | |
| 303 | <h2 className="mt-14 text-xl font-semibold tracking-tight">Limits, and how to raise them</h2> |
| 304 | <p className="mt-1 max-w-3xl text-sm text-muted"> |
| 305 | g1t lets usage run ahead of payment only so far. Limits keep a mistake small, for you and for g1t. |
| 306 | </p> |
| 307 | <ul className="mt-4 grid gap-3 text-sm sm:grid-cols-2"> |
| 308 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 309 | <p className="font-medium">{dollars(tier.paidStartCeilingMicros)} in the first month</p> |
| 310 | <p className="mt-1 text-muted"> |
| 311 | A workspace on the plan can use up to {dollars(tier.paidStartCeilingMicros)} in its first month. After that, |
| 312 | the limit grows with payments that clear. |
| 313 | </p> |
| 314 | </li> |
| 315 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 316 | <p className="font-medium">Your own spend limit</p> |
| 317 | <p className="mt-1 text-muted"> |
| 318 | Owners set a monthly spend limit anywhere up to the highest limit the workspace has had, with no approval. |
| 319 | Once, you can raise it yourself to twice that. |
| 320 | </p> |
| 321 | </li> |
| 322 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 323 | <p className="font-medium">Prepay to raise it now</p> |
| 324 | <p className="mt-1 text-muted"> |
| 325 | Prepaying from $25 raises what you can use by the same amount, at once. From $1,000 you can also pay by bank |
| 326 | transfer. |
| 327 | </p> |
| 328 | </li> |
| 329 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 330 | <p className="font-medium">Raise my limit</p> |
| 331 | <p className="mt-1 text-muted"> |
| 332 | Need more? Ask from Billing with the amount, why, and what you expect to spend. You get an answer within one |
| 333 | business day, in the app and by email. |
| 334 | </p> |
| 335 | </li> |
| 336 | </ul> |
| 337 | |
| 338 | <h2 className="mt-14 text-xl font-semibold tracking-tight">When usage runs high</h2> |
| 339 | <ul className="mt-4 grid gap-3 text-sm sm:grid-cols-2"> |
| 340 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 341 | <p className="font-medium">A spike pauses new work</p> |
| 342 | <p className="mt-1 text-muted"> |
| 343 | When an hour's spend is five times your usual hour, and at least $5, new compute waits for an owner to choose |
| 344 | Keep going or Stop. Runs already going finish. |
| 345 | </p> |
| 346 | </li> |
| 347 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 348 | <p className="font-medium">Caps per run and per issue</p> |
| 349 | <p className="mt-1 text-muted"> |
| 350 | One agent run stops at $2 and the agents on one issue at $10 in all, so a run stuck in a loop stops on its own. |
| 351 | Owners change both on Billing. |
| 352 | </p> |
| 353 | </li> |
| 354 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 355 | <p className="font-medium">Alerts at 50, 75, 90 and 100%</p> |
| 356 | <p className="mt-1 text-muted"> |
| 357 | Of the plan's included usage, your spend limit and g1t's limit: in the app and by email. |
| 358 | </p> |
| 359 | </li> |
| 360 | <li className="rounded-xl border border-line bg-surface p-4"> |
| 361 | <p className="font-medium">Spent more than you meant to? Tell us</p> |
| 362 | <p className="mt-1 text-muted"> |
| 363 | Tell us from Billing what happened. Once in 12 months, g1t can credit back usage you did not mean, always |
| 364 | including its 20% on it. Larger credits, or a second one within 12 months, are reviewed by a person. |
| 365 | </p> |
| 366 | </li> |
| 367 | </ul> |
| 368 | <p className="mt-3 text-sm text-muted"> |
| 369 | No card is charged less than {dollars(tier.minChargeMicros)} when a month closes, so a payment's fee is never most |
| 370 | of it. Smaller amounts carry over to the next invoice. Charges at a limit always go through. |
| 371 | </p> |
| 372 | |
| 373 | <div className="mt-14 grid gap-4 sm:grid-cols-2"> |
| 374 | {HOW.map((item) => ( |
| 375 | <section key={item.title} className="rounded-xl border border-line bg-surface p-5"> |
| 376 | <h2 className="font-medium">{item.title}</h2> |
| 377 | <p className="mt-1.5 text-sm text-muted">{item.body}</p> |
| 378 | </section> |
| 379 | ))} |
| 380 | </div> |
| 381 | |
| 382 | <h2 className="mt-14 text-xl font-semibold tracking-tight">Usage</h2> |
| 383 | <p className="mt-1 text-sm text-muted"> |
| 384 | {checked ? ( |
| 385 | <> |
| 386 | Last checked against Cloudflare's bill <TimeAgo at={checked} />. |
| 387 | </> |
| 388 | ) : ( |
| 389 | "Starting from Cloudflare's published prices; checked against its bill daily." |
| 390 | )} |
| 391 | </p> |
| 392 | <div className="mt-4 overflow-x-auto rounded-xl border border-line"> |
| 393 | <table className="w-full text-left text-sm sm:min-w-[36rem]"> |
| 394 | <thead className="border-b border-line text-xs text-muted"> |
| 395 | <tr> |
| 396 | <th className="px-4 py-2.5 font-medium">What</th> |
| 397 | <th className="px-4 py-2.5 font-medium">Costs g1t</th> |
| 398 | {/* The markup is in what you pay; on a phone the column gives way. */} |
| 399 | <th className="hidden px-4 py-2.5 font-medium sm:table-cell">Markup</th> |
| 400 | <th className="px-4 py-2.5 font-medium">You pay</th> |
| 401 | </tr> |
| 402 | </thead> |
| 403 | <tbody className="divide-y divide-line"> |
| 404 | <tr> |
| 405 | <td className="px-4 py-3"> |
| 406 | <p className="font-medium">Models</p> |
| 407 | <p className="text-xs text-faint">g1t's hosted models, through AI Gateway</p> |
| 408 | </td> |
| 409 | <td className="px-4 py-3 text-muted">What the provider charges, per request</td> |
| 410 | <td className="hidden px-4 py-3 tabular-nums sm:table-cell">{book?.modelMarginPercent ?? 20}%</td> |
| 411 | <td className="px-4 py-3 text-muted">Cost + {book?.modelMarginPercent ?? 20}%</td> |
| 412 | </tr> |
| 413 | {(book?.prices ?? []).map((price) => ( |
| 414 | <tr key={price.meter}> |
| 415 | <td className="px-4 py-3"> |
| 416 | <p className="font-medium">{price.title}</p> |
| 417 | <p className="text-xs text-faint"> |
| 418 | {price.source === "cloudflare" ? "Measured from Cloudflare's bill" : "Cloudflare's published price"} |
| 419 | </p> |
| 420 | </td> |
| 421 | <td className="px-4 py-3 font-mono text-xs tabular-nums text-muted">{perUnit(price, price.costMicros)}</td> |
| 422 | <td className="hidden px-4 py-3 tabular-nums sm:table-cell">{price.markupPercent}%</td> |
| 423 | <td className="px-4 py-3 font-mono text-xs tabular-nums">{perUnit(price, price.priceMicros)}</td> |
| 424 | </tr> |
| 425 | ))} |
| 426 | </tbody> |
| 427 | </table> |
| 428 | </div> |
| 429 | |
| 430 | <h2 className="mt-14 text-xl font-semibold tracking-tight">Enterprise</h2> |
| 431 | <p className="mt-1 text-sm text-muted"> |
| 432 | For organizations with several workspaces: one invoice and one limit. The prices are the ones on this page. |
| 433 | </p> |
| 434 | <section className="mt-4 rounded-xl border border-line bg-surface p-5"> |
| 435 | <ul className="grid gap-3 text-sm sm:grid-cols-2"> |
| 436 | <li> |
| 437 | <p className="font-medium">Consolidated invoicing</p> |
| 438 | <p className="text-muted">One monthly invoice for every workspace, paid by card or bank transfer.</p> |
| 439 | </li> |
| 440 | <li> |
| 441 | <p className="font-medium">Custom terms</p> |
| 442 | <p className="text-muted">One limit across every workspace, invoice terms and a billing contact, set with you.</p> |
| 443 | </li> |
| 444 | <li> |
| 445 | <p className="font-medium">Security on every plan</p> |
| 446 | <p className="text-muted"> |
| 447 | The audit log for {tier.auditRetentionDays} days with export, and secret push protection, for every workspace. |
| 448 | </p> |
| 449 | </li> |
| 450 | <li> |
| 451 | <p className="font-medium"> |
| 452 | Single sign-on <span className="ml-1 rounded bg-raised px-1.5 py-0.5 text-xs text-muted">Coming</span> |
| 453 | </p> |
| 454 | <p className="text-muted">Through your identity provider, on every plan once it is built. Not available yet.</p> |
| 455 | </li> |
| 456 | </ul> |
| 457 | <a |
| 458 | href={ENTERPRISE_MAIL} |
| 459 | className="mt-5 inline-flex items-center gap-1.5 rounded-md border border-line px-3 py-1.5 text-sm hover:border-line-strong" |
| 460 | > |
| 461 | Write to us about enterprise |
| 462 | <ArrowUpRight size={14} /> |
| 463 | </a> |
| 464 | </section> |
| 465 | |
| 466 | <h2 className="mt-14 text-xl font-semibold tracking-tight">Price changes</h2> |
| 467 | {book && book.changes.length > 0 ? ( |
| 468 | <ul className="mt-4 divide-y divide-line rounded-xl border border-line"> |
| 469 | {book.changes.map((change) => { |
| 470 | const title = book.prices.find((p) => p.meter === change.meter)?.title ?? change.meter; |
| 471 | // What you pay moved: the cost, the markup, or both. |
| 472 | const before = change.oldCostMicros * (100 + (change.oldMarkupPercent ?? change.markupPercent)); |
| 473 | const after = change.newCostMicros * (100 + change.markupPercent); |
| 474 | const up = after > before; |
| 475 | const fresh = after === before || before === 0; |
| 476 | return ( |
| 477 | <li key={`${change.meter}-${change.createdAt}`} className="px-4 py-3 text-sm"> |
| 478 | <p> |
| 479 | <span className="font-medium">{title}</span>{" "} |
| 480 | {fresh ? ( |
| 481 | <span className="text-muted">new</span> |
| 482 | ) : ( |
| 483 | <span className={up ? "text-warn" : "text-accent"}> |
| 484 | {up ? "up" : "down"} {Math.abs((after / before - 1) * 100).toFixed(1)}% |
| 485 | </span> |
| 486 | )} |
| 487 | </p> |
| 488 | <p className="mt-0.5 text-xs text-faint"> |
| 489 | {change.reason} · <TimeAgo at={change.createdAt} /> |
| 490 | </p> |
| 491 | </li> |
| 492 | ); |
| 493 | })} |
| 494 | </ul> |
| 495 | ) : ( |
| 496 | <p className="mt-2 text-sm text-muted">None yet. When a price moves, it is listed here with why.</p> |
| 497 | )} |
| 498 | |
| 499 | <a |
| 500 | href="https://docs.g1t.sh/guides/usage-and-billing/" |
| 501 | className="mt-12 inline-flex items-center gap-1.5 text-sm text-accent hover:underline" |
| 502 | > |
| 503 | How usage and billing work |
| 504 | <ArrowUpRight size={14} /> |
| 505 | </a> |
| 506 | </main> |
| 507 | ); |
| 508 | } |