Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

pricing.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

pricing.tsxBlame641 lines · 1 contributor
5f2e749Claude1/**
2 * Block L8 — public `/pricing` page.
3 *
4 * Anonymous-safe GET /pricing. Reads the real plan rows from
5 * `billing_plans` (or `FALLBACK_PLANS` when seeds aren't loaded yet — both
6 * mirror migration 0020) so the price column never drifts from the actual
7 * billing config. This route is mounted BEFORE `routes/marketing.tsx` in
8 * `app.tsx` so the new editorial layout wins; the legacy marketing /pricing
9 * remains as a safety net.
10 *
11 * Anchors:
12 * #free → "What you get on the free tier" block
13 * #self-host → Self-host vs Cloud comparison
14 * #faq → Frequently asked questions
15 *
16 * Pure presentational — no billing logic is created here. The page only
17 * surfaces what `src/lib/billing.ts` already ships.
18 */
19
20import { Hono } from "hono";
21import type { FC } from "hono/jsx";
22import { Layout } from "../views/layout";
23import { softAuth } from "../middleware/auth";
24import type { AuthEnv } from "../middleware/auth";
25import { formatPrice, listPlans } from "../lib/billing";
26
27const pricing = new Hono<AuthEnv>();
28pricing.use("*", softAuth);
29
30// ---- Per-slug copy: tagline + included-bullet list ------------------------
31// Indexed by the seeded plan slugs. Anything not in this map falls back to
32// generic copy derived from the plan's numeric limits so we never miss a
33// row even if a future migration adds a new tier.
34const PLAN_COPY: Record<
35 string,
36 { tagline: string; supportTier: string }
37> = {
38 free: {
39 tagline: "Personal projects + open source. Full AI suite.",
40 supportTier: "Community support",
41 },
42 pro: {
43 tagline: "Working developers shipping every day.",
44 supportTier: "Email support, priority AI queue",
45 },
46 team: {
47 tagline: "Teams running production on Gluecron.",
48 supportTier: "Slack channel + 24h response",
49 },
50 enterprise: {
51 tagline: "Orgs that need SSO, audit, on-prem.",
52 supportTier: "24/7 incident response + DPA",
53 },
54};
55
56pricing.get("/pricing", async (c) => {
57 const user = c.get("user");
58 const plans = await listPlans();
59 return c.html(
60 <Layout title="Pricing — Gluecron" user={user}>
61 <PricingPage plans={plans} loggedIn={!!user} />
62 </Layout>
63 );
64});
65
66interface Plan {
67 slug: string;
68 name: string;
69 priceCents: number;
70 repoLimit: number;
71 storageMbLimit: number;
72 aiTokensMonthly: number;
73 bandwidthGbMonthly: number;
74 privateRepos: boolean;
75}
76
77const PricingPage: FC<{ plans: Plan[]; loggedIn: boolean }> = ({
78 plans,
79 loggedIn,
80}) => (
81 <>
82 <style dangerouslySetInnerHTML={{ __html: pricingCss }} />
83 <div class="pl-root">
84 {/* ------------------- Hero ------------------- */}
85 <header class="pl-hero">
86 <div class="eyebrow">Pricing</div>
87 <h1 class="display pl-hero-title">
88 Free for the AI-curious.{" "}
89 <span class="gradient-text">Pay only when you're ready to scale.</span>
90 </h1>
91 <p class="pl-hero-sub">
92 Self-host on your own server and pay zero per-seat fees. Or use
93 Gluecron Cloud for managed convenience.
94 </p>
95 <div class="pl-hero-jumps">
96 <a href="#free" class="pl-jump">What's free →</a>
97 <a href="#self-host" class="pl-jump">Self-host vs Cloud</a>
98 <a href="#faq" class="pl-jump">FAQ</a>
99 </div>
100 </header>
101
102 {/* ------------------- Plan cards ------------------- */}
103 <section class="pl-plans stagger">
104 {plans.map((p) => (
105 <PlanCard plan={p} loggedIn={loggedIn} />
106 ))}
107 </section>
108
109 {/* ------------------- What's on the free tier ------------------- */}
110 <section id="free" class="pl-section pl-free">
111 <div class="section-header">
112 <div class="eyebrow">Free tier</div>
113 <h2>Everything below is yours on the free tier.</h2>
114 <p>
115 All the AI features. Not a "try it for 14 days" trial. Not a
116 "core features" stub. The whole Claude-powered platform — on
117 unlimited public repos, forever.
118 </p>
119 </div>
120 <ul class="pl-free-grid">
121 <FreeItem label="Unlimited public repos" />
122 <FreeItem label="AI code review on every PR (Sonnet 4)" />
123 <FreeItem label="AI auto-merge when checks pass (K2)" />
124 <FreeItem label="ai:build label → spec-to-PR (K3)" />
125 <FreeItem label="Sleep Mode digest (L1)" />
126 <FreeItem label="AI hours saved counter (L9)" />
127 <FreeItem label="MCP server access (K1)" />
128 <FreeItem label="Claude Code skill bundle (L7)" />
129 <FreeItem label="One-command install" />
130 <FreeItem label="GitHub OIDC sign-in" />
131 <FreeItem label="Webhooks + REST API v2 + GraphQL" />
132 <FreeItem label="Package registry + Pages hosting" />
133 </ul>
134 </section>
135
136 {/* ------------------- Self-host vs Cloud ------------------- */}
137 <section id="self-host" class="pl-section">
138 <div class="section-header">
139 <div class="eyebrow">Two ways to run it</div>
140 <h2>Self-host on your metal. Or let us run it.</h2>
141 <p>
142 Same product, same code, same Claude-powered features. The only
143 difference is who pays the electricity bill.
144 </p>
145 </div>
146 <div class="pl-host-grid">
147 <div class="pl-host-col">
148 <div class="pl-host-name">Self-host</div>
149 <div class="pl-host-price">Free forever</div>
150 <ul class="pl-host-feats">
151 <li>Free forever — no license, no per-seat fee</li>
152 <li>Your database, your disk, your control</li>
153 <li>You pay your Anthropic API key directly</li>
154 <li>Run via <code>curl gluecron.com/install</code></li>
155 <li>Or the Hetzner bootstrap script in 30 seconds</li>
156 </ul>
157 <a href="/install" class="btn btn-secondary btn-block pl-host-cta">
158 Self-host guide
159 </a>
160 </div>
161 <div class="pl-host-col pl-host-cloud">
162 <div class="pl-host-name">Gluecron Cloud</div>
163 <div class="pl-host-price">From $0/mo</div>
164 <ul class="pl-host-feats">
165 <li>Managed — we run the server, you push code</li>
166 <li>Opinionated stack, zero ops on your end</li>
167 <li>Automatic upgrades to every new block</li>
168 <li>Support included on paid plans</li>
169 <li>Plan-based pricing, no surprise overage</li>
170 </ul>
171 <a
172 href={loggedIn ? "/settings/billing" : "/register?next=/settings/billing"}
173 class="btn btn-primary btn-block pl-host-cta"
174 >
175 Start on Cloud
176 </a>
177 </div>
178 </div>
179 </section>
180
181 {/* ------------------- FAQ ------------------- */}
182 <section id="faq" class="pl-section">
183 <div class="section-header">
184 <div class="eyebrow">Questions</div>
185 <h2>The fine print, in plain English.</h2>
186 </div>
187 <div class="pl-faq">
188 <FaqItem
189 q="Is it really free? What's the catch?"
190 a="Really free. The free tier exists because we want every Claude-curious developer to try Gluecron without a credit card. The catch — if you can call it that — is that we hope you'll upgrade to Pro once you're shipping production traffic and need higher AI quotas."
191 />
192 <FaqItem
193 q="Do I need to bring my own Anthropic API key on the free tier?"
194 a="No. The free tier includes a generous monthly AI quota powered by our keys. If you'd rather use your own key (for cost control or enterprise rate limits), you can plug it in at /settings — Pro and above can route AI through your account."
195 />
196 <FaqItem
197 q="What happens when I exceed my plan's quota?"
198 a="AI features degrade gracefully — git push, hosting, and gates keep working. AI suggestions queue at the back of the line until the next cycle. We never auto-bill you for overage or auto-upgrade your plan."
199 />
200 <FaqItem
201 q="Can I migrate from GitHub for free?"
202 a="Yes. The migration tool is on every tier, free included. Point it at a GitHub repo URL and we mirror code, issues, PRs, and releases in one shot. No vendor lock — you can migrate back the same way."
203 />
204 <FaqItem
205 q="Does the free tier include private repos?"
206 a="The free tier is built around unlimited public repos. Private repos start on the Pro plan — that's the main paid-tier perk along with the higher AI quota. If you're self-hosting, all repos are private by default and there's no plan to worry about."
207 />
208 </div>
209 </section>
210
211 {/* ------------------- CTA ------------------- */}
212 <section class="pl-section pl-cta-wrap">
213 <div class="pl-cta">
214 <h2 class="pl-cta-title">
215 Ready to push your first repo?
216 </h2>
217 <p class="pl-cta-sub">
218 Free, no credit card, full AI suite from minute one.
219 </p>
220 <div class="pl-cta-buttons">
221 <a href="/register" class="btn btn-primary btn-xl">
222 Start free
223 </a>
224 <a href="/vs-github" class="btn btn-ghost btn-xl">
225 Compare to GitHub
226 </a>
227 </div>
228 </div>
229 </section>
230 </div>
231 </>
232);
233
234// ---- Sub-components -------------------------------------------------------
235
236const PlanCard: FC<{ plan: Plan; loggedIn: boolean }> = ({ plan, loggedIn }) => {
237 const copy = PLAN_COPY[plan.slug] || {
238 tagline: `${plan.name} plan.`,
239 supportTier: "Support included",
240 };
241 const href = loggedIn
242 ? `/settings/billing?plan=${plan.slug}`
243 : `/register?next=/settings/billing?plan=${plan.slug}`;
244 const isPro = plan.slug === "pro";
245 return (
246 <div class={`pl-card${isPro ? " pl-card-hl" : ""}`}>
247 {isPro && <div class="pl-card-badge">Most popular</div>}
248 <div class="pl-card-name">{plan.name}</div>
249 <div class="pl-card-price">
250 <span class="pl-card-num">{formatPrice(plan.priceCents)}</span>
251 </div>
252 <p class="pl-card-tag">{copy.tagline}</p>
253 <ul class="pl-card-feats">
254 <li>
255 <span class="pl-check">{"✓"}</span>
256 {plan.repoLimit.toLocaleString()} repos
257 {plan.privateRepos ? " (public + private)" : " (public only)"}
258 </li>
259 <li>
260 <span class="pl-check">{"✓"}</span>
261 {plan.aiTokensMonthly.toLocaleString()} AI tokens / month
262 </li>
263 <li>
264 <span class="pl-check">{"✓"}</span>
265 {plan.storageMbLimit.toLocaleString()} MB storage
266 </li>
267 <li>
268 <span class="pl-check">{"✓"}</span>
269 {plan.bandwidthGbMonthly.toLocaleString()} GB bandwidth / month
270 </li>
271 <li>
272 <span class="pl-check">{"✓"}</span>
273 {copy.supportTier}
274 </li>
275 </ul>
276 <a
277 href={href}
278 class={`btn ${isPro ? "btn-primary" : "btn-secondary"} btn-block pl-card-cta`}
279 >
280 Choose {plan.name}
281 </a>
282 </div>
283 );
284};
285
286const FreeItem: FC<{ label: string }> = ({ label }) => (
287 <li class="pl-free-item">
288 <span class="pl-free-check">{"✓"}</span>
289 <span>{label}</span>
290 </li>
291);
292
293const FaqItem: FC<{ q: string; a: string }> = ({ q, a }) => (
294 <details class="pl-faq-item">
295 <summary class="pl-faq-q">
296 <span>{q}</span>
297 <span class="pl-faq-toggle" aria-hidden="true">{"+"}</span>
298 </summary>
299 <p class="pl-faq-a">{a}</p>
300 </details>
301);
302
303// ---- Styles (scoped under .pl-) -------------------------------------------
304
305const pricingCss = `
306 .pl-root { max-width: 1180px; margin: 0 auto; padding: 0 16px; }
307
308 /* Hero */
309 .pl-hero {
310 text-align: center;
311 padding: var(--s-16) 0 var(--s-10);
312 max-width: 920px;
313 margin: 0 auto;
314 position: relative;
315 }
316 .pl-hero::before {
317 content: '';
318 position: absolute;
319 top: 0; left: 50%;
320 transform: translateX(-50%);
321 width: 70%; height: 60%;
322 background: radial-gradient(ellipse at center, rgba(140,109,255,0.14), transparent 65%);
323 z-index: -1;
324 pointer-events: none;
325 }
326 .pl-hero .eyebrow { justify-content: center; margin: 0 auto var(--s-4); }
327 .pl-hero-title {
328 font-size: clamp(36px, 6.5vw, 72px);
329 line-height: 1.02;
330 letter-spacing: -0.038em;
331 margin: 0 0 var(--s-5);
332 }
333 .pl-hero-sub {
334 font-size: clamp(15px, 1.5vw, 18px);
335 color: var(--text-muted);
336 max-width: 640px;
337 margin: 0 auto;
338 line-height: 1.55;
339 }
340 .pl-hero-jumps {
341 display: flex;
342 gap: 18px;
343 justify-content: center;
344 flex-wrap: wrap;
345 margin-top: var(--s-7);
346 }
347 .pl-jump {
348 font-family: var(--font-mono);
349 font-size: 12px;
350 color: var(--text-muted);
351 text-decoration: none;
352 padding: 6px 12px;
353 border: 1px solid var(--border-subtle);
354 border-radius: var(--r-full);
355 transition: color var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
356 }
357 .pl-jump:hover { color: var(--accent); border-color: rgba(140,109,255,0.35); }
358
359 /* Plan cards */
360 .pl-plans {
361 display: grid;
362 grid-template-columns: repeat(4, 1fr);
363 gap: 14px;
364 margin: var(--s-10) auto var(--s-14);
365 align-items: stretch;
366 }
367 .pl-card {
368 position: relative;
369 background: var(--bg-elevated);
370 border: 1px solid var(--border);
371 border-radius: var(--r-lg);
372 padding: var(--s-7) var(--s-6);
373 display: flex;
374 flex-direction: column;
375 gap: var(--s-4);
376 transition: border-color var(--t-base) var(--ease), transform var(--t-base) var(--ease-out-quart);
377 }
378 .pl-card:hover { border-color: var(--border-strong); transform: translateY(-3px); }
379 .pl-card-hl {
380 border-color: rgba(140,109,255,0.40);
381 box-shadow: var(--elev-2), 0 0 0 1px rgba(140,109,255,0.30);
382 background:
383 linear-gradient(180deg, rgba(140,109,255,0.05), transparent 50%),
384 var(--bg-elevated);
385 }
386 .pl-card-badge {
387 position: absolute;
388 top: -10px;
389 left: 50%;
390 transform: translateX(-50%);
391 padding: 3px 12px;
392 background: var(--accent-gradient);
393 color: #fff;
394 font-family: var(--font-mono);
395 font-size: 10px;
396 letter-spacing: 0.1em;
397 text-transform: uppercase;
398 font-weight: 600;
399 border-radius: var(--r-full);
400 box-shadow: 0 4px 14px -2px rgba(140,109,255,0.45);
401 white-space: nowrap;
402 }
403 .pl-card-name {
404 font-family: var(--font-mono);
405 font-size: 11px;
406 text-transform: uppercase;
407 letter-spacing: 0.16em;
408 color: var(--text-muted);
409 }
410 .pl-card-price { display: flex; align-items: baseline; gap: 6px; }
411 .pl-card-num {
412 font-family: var(--font-display);
413 font-size: 32px;
414 font-weight: 600;
415 letter-spacing: -0.03em;
416 color: var(--text-strong);
417 }
418 .pl-card-tag {
419 font-size: var(--t-sm);
420 color: var(--text-muted);
421 line-height: 1.5;
422 margin: 0;
423 }
424 .pl-card-feats {
425 list-style: none;
426 padding: 0;
427 margin: 0;
428 display: flex;
429 flex-direction: column;
430 gap: 7px;
431 font-size: 13px;
432 color: var(--text);
433 }
434 .pl-card-feats li {
435 display: flex;
436 align-items: flex-start;
437 gap: 9px;
438 line-height: 1.45;
439 }
440 .pl-check {
441 color: var(--accent);
442 font-weight: 600;
443 flex-shrink: 0;
444 line-height: 1.45;
445 }
446 .pl-card-cta { margin-top: auto; }
447
448 /* Section base */
449 .pl-section { margin: var(--s-14) auto; }
450
451 /* Free-tier block */
452 .pl-free-grid {
453 list-style: none;
454 padding: 0;
455 margin: var(--s-6) auto 0;
456 display: grid;
457 grid-template-columns: repeat(2, 1fr);
458 gap: 10px 32px;
459 max-width: 880px;
460 }
461 .pl-free-item {
462 display: flex;
463 align-items: flex-start;
464 gap: 10px;
465 padding: 12px 16px;
466 background: var(--bg-elevated);
467 border: 1px solid var(--border-subtle);
468 border-radius: var(--r);
469 font-size: var(--t-sm);
470 color: var(--text);
471 line-height: 1.45;
472 transition: border-color var(--t-fast) var(--ease);
473 }
474 .pl-free-item:hover { border-color: rgba(140,109,255,0.35); }
475 .pl-free-check {
476 color: var(--green);
477 font-weight: 700;
478 flex-shrink: 0;
479 }
480
481 /* Self-host vs Cloud */
482 .pl-host-grid {
483 display: grid;
484 grid-template-columns: 1fr 1fr;
485 gap: 16px;
486 max-width: 920px;
487 margin: 0 auto;
488 }
489 .pl-host-col {
490 background: var(--bg-elevated);
491 border: 1px solid var(--border);
492 border-radius: var(--r-lg);
493 padding: var(--s-7) var(--s-6);
494 display: flex;
495 flex-direction: column;
496 gap: var(--s-4);
497 }
498 .pl-host-cloud {
499 border-color: rgba(140,109,255,0.35);
500 box-shadow: 0 0 0 1px rgba(140,109,255,0.20);
501 background:
502 linear-gradient(180deg, rgba(140,109,255,0.04), transparent 60%),
503 var(--bg-elevated);
504 }
505 .pl-host-name {
506 font-family: var(--font-mono);
507 font-size: 11px;
508 text-transform: uppercase;
509 letter-spacing: 0.16em;
510 color: var(--text-muted);
511 }
512 .pl-host-price {
513 font-family: var(--font-display);
514 font-size: 28px;
515 font-weight: 600;
516 letter-spacing: -0.025em;
517 color: var(--text-strong);
518 }
519 .pl-host-feats {
520 list-style: none;
521 padding: 0;
522 margin: 0;
523 display: flex;
524 flex-direction: column;
525 gap: 8px;
526 font-size: var(--t-sm);
527 color: var(--text);
528 }
529 .pl-host-feats li {
530 display: flex;
531 gap: 9px;
532 line-height: 1.5;
533 }
534 .pl-host-feats li::before {
535 content: '→';
536 color: var(--accent);
537 flex-shrink: 0;
538 }
539 .pl-host-feats code {
540 background: var(--bg-secondary);
541 border: 1px solid var(--border-subtle);
542 padding: 1px 6px;
543 border-radius: 4px;
544 font-size: 11.5px;
545 font-family: var(--font-mono);
546 color: var(--accent);
547 }
548 .pl-host-cta { margin-top: auto; }
549
550 /* FAQ */
551 .pl-faq {
552 max-width: 760px;
553 margin: 0 auto;
554 border: 1px solid var(--border);
555 border-radius: var(--r-lg);
556 overflow: hidden;
557 background: var(--bg-elevated);
558 }
559 .pl-faq-item { border-bottom: 1px solid var(--border-subtle); }
560 .pl-faq-item:last-child { border-bottom: none; }
561 .pl-faq-q {
562 display: flex;
563 justify-content: space-between;
564 align-items: center;
565 gap: 16px;
566 padding: 18px 24px;
567 cursor: pointer;
568 font-size: var(--t-md);
569 font-weight: 500;
570 color: var(--text-strong);
571 list-style: none;
572 transition: background var(--t-fast) var(--ease);
573 }
574 .pl-faq-q::-webkit-details-marker { display: none; }
575 .pl-faq-q:hover { background: var(--bg-hover); }
576 .pl-faq-toggle {
577 font-family: var(--font-mono);
578 font-size: 18px;
579 color: var(--text-muted);
580 transition: transform var(--t-base) var(--ease-spring);
581 flex-shrink: 0;
582 }
583 .pl-faq-item[open] .pl-faq-toggle { transform: rotate(45deg); color: var(--accent); }
584 .pl-faq-a {
585 padding: 0 24px 20px;
586 color: var(--text-muted);
587 font-size: var(--t-sm);
588 line-height: 1.6;
589 margin: 0;
590 }
591
592 /* CTA */
593 .pl-cta-wrap { margin: var(--s-16) auto var(--s-10); }
594 .pl-cta {
595 position: relative;
596 text-align: center;
597 padding: var(--s-12) var(--s-6);
598 border: 1px solid var(--border-strong);
599 border-radius: var(--r-2xl);
600 background:
601 radial-gradient(60% 100% at 50% 0%, rgba(140,109,255,0.14), transparent 65%),
602 var(--bg-elevated);
603 overflow: hidden;
604 }
605 .pl-cta-title {
606 font-family: var(--font-display);
607 font-size: clamp(24px, 3.5vw, 40px);
608 line-height: 1.1;
609 letter-spacing: -0.025em;
610 font-weight: 600;
611 margin: 0 0 var(--s-3);
612 color: var(--text-strong);
613 }
614 .pl-cta-sub {
615 font-size: var(--t-md);
616 color: var(--text-muted);
617 margin: 0 auto var(--s-6);
618 max-width: 480px;
619 }
620 .pl-cta-buttons {
621 display: flex;
622 gap: 12px;
623 justify-content: center;
624 flex-wrap: wrap;
625 }
626
627 /* Responsive */
628 @media (max-width: 960px) {
629 .pl-plans { grid-template-columns: repeat(2, 1fr); }
630 }
631 @media (max-width: 720px) {
632 .pl-host-grid { grid-template-columns: 1fr; }
633 .pl-free-grid { grid-template-columns: 1fr; }
634 }
635 @media (max-width: 560px) {
636 .pl-plans { grid-template-columns: 1fr; }
637 .pl-cta-buttons .btn { width: 100%; justify-content: center; }
638 }
639`;
640
641export default pricing;