Skip to content

Commit 20b88a4

Browse files
madfam-ioaldoruizlunaclaude
authored
fix(website): janua /pricing renders + shows ratified $29 Managed tier (#454)
The /pricing route (app/(marketing)/pricing/page.tsx) exists and builds fine — `pnpm --filter @janua/website build` prerenders `○ /pricing`. The prod 404 is therefore deploy-lag (prod runs an older janua-website image that predates the route), not a routing/build error. Needs a website promote to serve it; code is correct on main. Correct displayed prices to the ratified enclii+janua packaging decision (internal-devops decisions/2026-07-11-enclii-janua-packaging-ratification.md): - Managed (Pro): $69 → $29/mo (flat per project, up to 10,000 MAU fair use; NOT per-MAU metered) - Scale $299 (self-serve) → Business (SSO) $149/mo, design-partner / contract — CTA routes to /contact, not self-serve signup - Open Source $0 and Enterprise custom unchanged Applied consistently across every live price surface: pricing.tsx (/pricing), honest-pricing.tsx (home), plus comparison.tsx, accurate-comparison.tsx and pain-points.tsx. Removed the now-inaccurate per-MAU overage section (Pro is flat per-org) and softened the startup-discount copy to the design-partner program, matching the existing "Private Alpha" honest positioning. Paid Pro CTA carries `?plan=pro` to app.janua.dev/auth/signup. Updated pricing.test.tsx (Scale→Business, asserts $29 present and $69/$299 absent). Co-authored-by: Aldo R. L. <aldo.ruiz.luna@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent eb004a6 commit 20b88a4

6 files changed

Lines changed: 135 additions & 80 deletions

File tree

apps/website/components/sections/accurate-comparison.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const rows: Row[] = [
135135
},
136136
{
137137
feature: 'Paid entry tier (per month)',
138-
janua: '$69 / 10k MAU',
138+
janua: '$29 / 10k MAU',
139139
auth0: '$240+',
140140
clerk: '$25 + per-MAU',
141141
keycloak: 'Self-operate',
@@ -256,7 +256,7 @@ export function AccurateComparison() {
256256
</h3>
257257
<ul className="space-y-2 text-sm text-slate-600 dark:text-slate-400">
258258
<li>You keep the user table. Self-host on Postgres you operate.</li>
259-
<li>One bill at $69/$299, not per-MAU metered SaaS pricing.</li>
259+
<li>One flat bill at $29/$149, not per-MAU metered SaaS pricing.</li>
260260
<li>SCIM v2 + audit retention without an enterprise upsell.</li>
261261
<li>9 client SDKs in the same monorepo as the API.</li>
262262
</ul>

apps/website/components/sections/comparison.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,19 @@ export function ComparisonSection() {
8686
supabase: '50,000'
8787
},
8888
{
89-
// Pro = $69/mo per PRICING_TIERS (apps/api). Includes 10,000 MAU.
90-
metric: 'Pro Tier Price',
91-
janua: '$69/mo',
89+
// Managed (Pro) = $29/mo per the ratified packaging decision
90+
// (internal-devops decisions/2026-07-11). Flat per project, up to
91+
// 10,000 MAU under fair use.
92+
metric: 'Paid Entry Price',
93+
janua: '$29/mo',
9294
clerk: '$99/mo',
9395
auth0: '$240/mo',
9496
supabase: '$25/mo'
9597
},
9698
{
99+
// Janua Pro is flat per-org, NOT per-MAU metered (ratified model).
97100
metric: 'Overage Cost/1k MAU',
98-
janua: '$10',
101+
janua: 'Flat, no meter',
99102
clerk: '$20',
100103
auth0: '$28',
101104
supabase: '$0.00325'

apps/website/components/sections/honest-pricing.tsx

Lines changed: 55 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ interface PricingTier {
1818
honestNote?: string
1919
}
2020

21-
// Pricing aligned with the canonical billing service tiers
22-
// (apps/api/app/services/billing_service.py PRICING_TIERS) and the
23-
// /pricing page (apps/website/components/sections/pricing.tsx).
21+
// Pricing aligned with the ratified enclii+janua packaging decision
22+
// (internal-devops decisions/2026-07-11-enclii-janua-packaging-ratification.md)
23+
// and the /pricing page (apps/website/components/sections/pricing.tsx).
2424
//
25-
// Community: Free, 2,000 MAU
26-
// Pro: $69/mo, 10,000 MAU
27-
// Scale: $299/mo, 50,000 MAU
28-
// Enterprise: Custom, Unlimited
25+
// Community (Open Source): Free, self-host, 2,000 MAU on managed
26+
// Pro (Managed): $29/mo flat per project, up to 10,000 MAU (fair use)
27+
// Business (SSO): $149/mo — design-partner / contract, NOT self-serve
28+
// Enterprise: Custom, Unlimited
2929
const pricingTiers: PricingTier[] = [
3030
{
3131
name: 'Community',
@@ -51,9 +51,9 @@ const pricingTiers: PricingTier[] = [
5151
},
5252
{
5353
name: 'Pro',
54-
price: '$69',
55-
description: 'For growing startups and teams',
56-
mau: '10,000 MAU',
54+
price: '$29',
55+
description: 'Managed hosting for growing startups and teams',
56+
mau: 'Up to 10,000 MAU (flat, fair use)',
5757
badge: 'Most Popular',
5858
features: [
5959
'Everything in Community',
@@ -74,10 +74,11 @@ const pricingTiers: PricingTier[] = [
7474
honestNote: 'Scales with you as you find product-market fit'
7575
},
7676
{
77-
name: 'Scale',
78-
price: '$299',
79-
description: 'For scale-ups with compliance needs',
80-
mau: '50,000 MAU',
77+
name: 'Business',
78+
price: '$149',
79+
description: 'SSO/SAML for compliance-driven teams',
80+
mau: 'Higher volume',
81+
badge: 'Design partner',
8182
features: [
8283
'Everything in Pro',
8384
'SSO/SAML',
@@ -93,7 +94,7 @@ const pricingTiers: PricingTier[] = [
9394
'No custom contracts',
9495
'No dedicated support'
9596
],
96-
honestNote: 'Enterprise-class security without enterprise pricing'
97+
honestNote: 'Early access via our design-partner program — talk to us'
9798
},
9899
{
99100
name: 'Enterprise',
@@ -102,7 +103,7 @@ const pricingTiers: PricingTier[] = [
102103
mau: 'Unlimited',
103104
badge: 'Full Support',
104105
features: [
105-
'Everything in Scale',
106+
'Everything in Business',
106107
'SCIM provisioning',
107108
'Custom contracts & SLAs',
108109
'Dedicated support',
@@ -126,7 +127,7 @@ interface ComparisonRow {
126127
feature: string
127128
community: boolean | string
128129
pro: boolean | string
129-
scale: boolean | string
130+
business: boolean | string
130131
enterprise: boolean | string
131132
}
132133

@@ -135,49 +136,49 @@ const detailedComparison: ComparisonRow[] = [
135136
feature: 'Monthly Active Users',
136137
community: '2,000',
137138
pro: '10,000',
138-
scale: '50,000',
139+
business: '50,000',
139140
enterprise: 'Unlimited'
140141
},
141142
{
142143
feature: 'Passkeys (WebAuthn)',
143144
community: true,
144145
pro: true,
145-
scale: true,
146+
business: true,
146147
enterprise: true
147148
},
148149
{
149150
feature: 'MFA/TOTP',
150151
community: false,
151152
pro: true,
152-
scale: true,
153+
business: true,
153154
enterprise: true
154155
},
155156
{
156157
feature: 'OAuth Providers',
157158
community: 'Email/password only',
158159
pro: 'Social (OAuth)',
159-
scale: 'All',
160+
business: 'All',
160161
enterprise: 'All + Custom'
161162
},
162163
{
163164
feature: 'Edge Response Time',
164165
community: '<30ms*',
165166
pro: '<30ms*',
166-
scale: '<30ms*',
167+
business: '<30ms*',
167168
enterprise: '<30ms*'
168169
},
169170
{
170171
feature: 'Support',
171172
community: 'Community',
172173
pro: 'Email',
173-
scale: 'Priority',
174+
business: 'Priority',
174175
enterprise: 'Dedicated'
175176
},
176177
{
177178
feature: 'SLA',
178179
community: false,
179180
pro: false,
180-
scale: false,
181+
business: false,
181182
// SLA is only offered at the Enterprise tier and is negotiated per
182183
// contract. No public uptime percentage until SRE on-call + SLO
183184
// definitions are in place.
@@ -187,14 +188,14 @@ const detailedComparison: ComparisonRow[] = [
187188
feature: 'SAML SSO',
188189
community: false,
189190
pro: false,
190-
scale: true,
191+
business: true,
191192
enterprise: true
192193
},
193194
{
194195
feature: 'Audit Log Retention',
195196
community: false,
196197
pro: '30 days',
197-
scale: '90 days',
198+
business: '90 days',
198199
enterprise: 'Unlimited'
199200
}
200201
]
@@ -205,19 +206,27 @@ export function HonestPricing() {
205206

206207
const yearlyDiscount = 0.2 // 20% off
207208

208-
const getPrice = (price: string) => {
209+
// Business (SSO) is a design-partner / contract tier — not self-serve — so
210+
// it is shown with its flat monthly price but no self-serve yearly discount.
211+
const isContactTier = (tier: PricingTier) =>
212+
tier.name === 'Business' || tier.price === 'Custom'
213+
214+
const getPrice = (tier: PricingTier) => {
215+
const { price } = tier
209216
if (price === 'Free' || price === 'Custom') return price
210217
const numPrice = parseInt(price.replace('$', ''))
211-
if (billingCycle === 'yearly') {
218+
if (billingCycle === 'yearly' && !isContactTier(tier)) {
212219
const yearlyPrice = Math.round(numPrice * (1 - yearlyDiscount))
213220
return `$${yearlyPrice}`
214221
}
215222
return price
216223
}
217224

218-
const getPriceLabel = (price: string) => {
225+
const getPriceLabel = (tier: PricingTier) => {
226+
const { price } = tier
219227
if (price === 'Free') return ''
220228
if (price === 'Custom') return 'Contact us'
229+
if (tier.name === 'Business') return '/mo · contact sales'
221230
return billingCycle === 'monthly' ? '/month' : '/month (billed annually)'
222231
}
223232

@@ -304,10 +313,10 @@ export function HonestPricing() {
304313
<div className="mb-6">
305314
<div className="flex items-baseline gap-1">
306315
<span className="text-3xl font-bold text-slate-900 dark:text-white">
307-
{getPrice(tier.price)}
316+
{getPrice(tier)}
308317
</span>
309318
<span className="text-sm text-slate-600 dark:text-slate-400">
310-
{getPriceLabel(tier.price)}
319+
{getPriceLabel(tier)}
311320
</span>
312321
</div>
313322
<p className="text-sm font-medium text-slate-700 dark:text-slate-300 mt-2">
@@ -356,16 +365,18 @@ export function HonestPricing() {
356365
>
357366
<Link
358367
href={
359-
tier.price === 'Custom'
368+
isContactTier(tier)
360369
? '/contact'
361370
: `https://app.janua.dev/auth/signup${
362-
tier.name === 'Pro' || tier.name === 'Scale'
363-
? `?plan=${tier.name.toLowerCase()}`
364-
: ''
371+
tier.name === 'Pro' ? '?plan=pro' : ''
365372
}`
366373
}
367374
>
368-
{tier.price === 'Custom' ? 'Contact Sales' : 'Get Started'}
375+
{tier.price === 'Custom'
376+
? 'Contact Sales'
377+
: tier.name === 'Business'
378+
? 'Contact us'
379+
: 'Get Started'}
369380
</Link>
370381
</Button>
371382
</motion.div>
@@ -402,7 +413,7 @@ export function HonestPricing() {
402413
Pro
403414
</th>
404415
<th className="text-center p-4 font-semibold text-slate-900 dark:text-white">
405-
Scale
416+
Business
406417
</th>
407418
<th className="text-center p-4 font-semibold text-slate-900 dark:text-white">
408419
Enterprise
@@ -445,15 +456,15 @@ export function HonestPricing() {
445456
)}
446457
</td>
447458
<td className="p-4 text-center">
448-
{typeof row.scale === 'boolean' ? (
449-
row.scale ? (
459+
{typeof row.business === 'boolean' ? (
460+
row.business ? (
450461
<Check className="w-4 h-4 text-green-600 mx-auto" />
451462
) : (
452463
<X className="w-4 h-4 text-slate-400 mx-auto" />
453464
)
454465
) : (
455466
<span className="text-sm text-slate-600 dark:text-slate-400">
456-
{row.scale}
467+
{row.business}
457468
</span>
458469
)}
459470
</td>
@@ -501,11 +512,12 @@ export function HonestPricing() {
501512

502513
<div>
503514
<h4 className="font-medium text-slate-700 dark:text-slate-300 mb-2">
504-
What happens if I exceed my MAU limit?
515+
What happens if I exceed the 10,000 MAU fair-use cap?
505516
</h4>
506517
<p className="text-sm text-slate-600 dark:text-slate-400">
507-
We'll notify you at 80% usage and help you upgrade. We never cut off authentication - your
508-
users won't be affected. Overages are billed at standard rates with no penalties.
518+
Pro is flat per project, not per-MAU metered. If you consistently run past the
519+
fair-use cap we&apos;ll reach out to move you to Business or a custom plan. We never cut off
520+
authentication - your users won&apos;t be affected.
509521
</p>
510522
</div>
511523

apps/website/components/sections/pain-points.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const pains = [
3838
pain_detail:
3939
'You don\'t want your identity layer behind a usage-based meter on someone else\'s database.',
4040
answer:
41-
'Self-host under AGPL-3.0. User table stays in your Postgres. Free up to 2,000 MAU, $69 Pro, $299 Scale, custom Enterprise on the managed plan.',
41+
'Self-host under AGPL-3.0. User table stays in your Postgres. Free up to 2,000 MAU, $29 Managed (Pro), $149 Business, custom Enterprise on the managed plan.',
4242
},
4343
]
4444

apps/website/components/sections/pricing.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,21 @@ describe('PricingSection', () => {
1111
render(<PricingSection />)
1212
expect(screen.getByText('Community')).toBeInTheDocument()
1313
expect(screen.getByText('Pro')).toBeInTheDocument()
14-
expect(screen.getByText('Scale')).toBeInTheDocument()
14+
expect(screen.getByText('Business')).toBeInTheDocument()
1515
expect(screen.getByText('Enterprise')).toBeInTheDocument()
1616
})
1717

18+
it('should show the ratified Managed (Pro) $29 price', () => {
19+
render(<PricingSection />)
20+
expect(screen.getAllByText('$29').length).toBeGreaterThan(0)
21+
})
22+
23+
it('should not show the retired $69 / $299 prices', () => {
24+
render(<PricingSection />)
25+
expect(screen.queryByText('$69')).not.toBeInTheDocument()
26+
expect(screen.queryByText('$299')).not.toBeInTheDocument()
27+
})
28+
1829
it('should show annual/monthly toggle', () => {
1930
render(<PricingSection />)
2031
expect(screen.getByText('Monthly')).toBeInTheDocument()

0 commit comments

Comments
 (0)