Skip to main content

max / makenotwork

server: route template tier prices through assumptions.toml; bump to 0.8.15 Plumb tier prices, founder rates, annual prices, file-size envelopes, and cohort cap from assumptions.toml into Askama templates via a new TierPrices struct on AppState. Closes the last drift class: a price change is now a one-line toml edit + restart, with no template, handler, or HTML touched. New module src/tier_prices.rs: - TierPrices struct with 25 fields: standard + founder monthly, standard + founder annual, per-tier per-file/total caps as strings, cohort cap display string. - from_assumptions(): reads every field from canonical keys, panics at startup on missing/wrong-type (same fail-fast pattern as Assumptions::validate). int_at/str_at helpers. - TierCard struct + cards() method: produces the 4-row vector the dashboard upgrade grid iterates, replacing a literal Askama tuple-array that hard-typed stale founder $5/$10/$15/$30 and standard $10/$20/$30/$60. - Unit test pinning every field to the canonical toml; guards against toml key removals or type flips at PR time instead of at startup. AppState (src/lib.rs): - Add pub tier_prices: TierPrices, populated in main.rs from the loaded Assumptions immediately after validation. Template structs that now carry tier_prices: - IndexTemplate, PricingTemplate, UseCasesTemplate, CreatorsTemplate, UserCreatorTabTemplate (the last via tier_cards: Vec<TierCard>). Handlers updated to pass state.tier_prices.clone() / state.tier_prices.cards(): - routes/pages/public/landing.rs (index, pricing_page, use_cases_page — pricing_page and use_cases_page now take State<AppState>). - routes/pages/public/mod.rs (creators_page). - routes/pages/dashboard/tabs/user/creator.rs (dashboard creator tab). Template substitutions across 5 HTML files: - pages/index.html: founder grid, "starts at $X/mo" pitch line, "$X-$Y" flat-tier-fee scale comparison. - pages/pricing.html: tier-card radios (value + price + envelope desc), calculator default summary text, cost-allocation row labels + aria labels (sub-amount breakdowns stay hand-typed pending §4.5.1 [cost_allocation] toml section), JS tierCost fallback. - pages/creators.html: pricing table with envelope-driven storage cells. - pages/use_cases.html: every use-case-tier pill substituted. - partials/tabs/user_creator.html: tier-card grid replaced literal Askama tuple-array iteration with {% for card in tier_cards %}; application-form tier-fit dropdown also substituted. Skipped intentionally: - index.html "Fan+ $8/mo with $5 monthly credit" — separate domain (Fan+ subscription pricing, not creator-tier pricing), not in assumptions.toml. - pricing.html cost-allocation sub-amounts ($0.76, $0.40, etc.) — separate refactor tracked in launchplan §4.5.1 (planned [cost_allocation] toml section). - pricing.html Ko-Fi comparison entry ($12/mo) — that's Ko-Fi's price, not MNW's. Production deploy: server restart picks up the new TierPrices from the existing ASSUMPTIONS_PATH toml. Already deployed via 0.8.14 push; this adds the in-template substitution layer that consumes the same keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-01 06:03 UTC
Commit: b7b2bd59a84276360a5943eceeec0bb0df496321
Parent: c083074
15 files changed, +273 insertions, -75 deletions
@@ -4140,7 +4140,7 @@ dependencies = [
4140 4140
4141 4141 [[package]]
4142 4142 name = "makenotwork"
4143 - version = "0.8.14"
4143 + version = "0.8.15"
4144 4144 dependencies = [
4145 4145 "anyhow",
4146 4146 "apple-codesign",
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.8.14"
3 + version = "0.8.15"
4 4 edition = "2024"
5 5 license-file = "LICENSE"
6 6
@@ -33,6 +33,7 @@ pub mod scanning;
33 33 pub mod storage;
34 34 pub mod synckit_auth;
35 35 pub mod templates;
36 + pub mod tier_prices;
36 37 pub mod types;
37 38 pub mod validation;
38 39 pub mod wordlist;
@@ -73,6 +74,7 @@ pub struct AppState {
73 74 pub stripe: Option<Arc<dyn PaymentProvider>>,
74 75 pub email: EmailClient,
75 76 pub docs: Arc<DocLoader>,
77 + pub tier_prices: tier_prices::TierPrices,
76 78 pub scanner: Option<Arc<ScanPipeline>>,
77 79 pub webauthn: Arc<Webauthn>,
78 80 pub syntax: Option<Arc<git::SyntaxHighlighter>>,
@@ -301,6 +301,7 @@ async fn main() {
301 301 stripe,
302 302 email,
303 303 docs,
304 + tier_prices: makenotwork::tier_prices::TierPrices::from_assumptions(&assumptions),
304 305 scanner,
305 306 webauthn,
306 307 syntax,
@@ -159,6 +159,7 @@ pub(in crate::routes::pages::dashboard) async fn dashboard_tab_creator(
159 159 founder_window_open: state.config.creator_founder_window_open,
160 160 is_founder: db_user.is_founder,
161 161 is_founder_locked: db_user.is_founder_locked(),
162 + tier_cards: state.tier_prices.cards(),
162 163 })
163 164 }
164 165
@@ -64,6 +64,7 @@ pub(super) async fn index(
64 64 total_items: total_items as u32,
65 65 founder_window_open,
66 66 founder_slots_remaining,
67 + tier_prices: state.tier_prices.clone(),
67 68 }.into_response())
68 69 }
69 70 }
@@ -348,9 +349,13 @@ pub(crate) async fn login_page(session: Session) -> impl IntoResponse {
348 349
349 350 /// Render the interactive pricing calculator page.
350 351 #[tracing::instrument(skip_all, name = "landing::pricing_page")]
351 - pub(super) async fn pricing_page(session: Session) -> impl IntoResponse {
352 + pub(super) async fn pricing_page(
353 + State(state): State<AppState>,
354 + session: Session,
355 + ) -> impl IntoResponse {
352 356 PricingTemplate {
353 357 csrf_token: get_csrf_token(&session).await,
358 + tier_prices: state.tier_prices.clone(),
354 359 }
355 360 }
356 361
@@ -385,12 +390,14 @@ pub(super) async fn checkout_complete() -> impl IntoResponse {
385 390 /// Render the use cases page.
386 391 #[tracing::instrument(skip_all, name = "landing::use_cases_page")]
387 392 pub(super) async fn use_cases_page(
393 + State(state): State<AppState>,
388 394 session: Session,
389 395 MaybeUserUnverified(maybe_user): MaybeUserUnverified,
390 396 ) -> impl IntoResponse {
391 397 UseCasesTemplate {
392 398 csrf_token: get_csrf_token(&session).await,
393 399 session_user: maybe_user,
400 + tier_prices: state.tier_prices.clone(),
394 401 }
395 402 }
396 403
@@ -127,5 +127,6 @@ async fn creators_page(
127 127 total_creators,
128 128 waitlist_pending,
129 129 is_creator,
130 + tier_prices: state.tier_prices.clone(),
130 131 })
131 132 }
@@ -335,6 +335,9 @@ pub struct UserCreatorTabTemplate {
335 335 pub is_founder: bool,
336 336 /// Whether this user's founder pricing is permanently locked in.
337 337 pub is_founder_locked: bool,
338 + /// Tier cards rendered in the upgrade grid. Built from `state.tier_prices`
339 + /// so a price change in `assumptions.toml` flows through automatically.
340 + pub tier_cards: Vec<crate::tier_prices::TierCard>,
338 341 }
339 342
340 343 /// Dashboard tab: project overview with stat cards.
@@ -54,6 +54,7 @@ pub struct IndexTemplate {
54 54 /// Remaining founder slots (1,000 cap). Only shown when small enough to
55 55 /// convey urgency; not exposed when comfortably above the cap.
56 56 pub founder_slots_remaining: Option<u32>,
57 + pub tier_prices: crate::tier_prices::TierPrices,
57 58 }
58 59
59 60 /// User's library shell with inline purchases tab (other tabs loaded via HTMX).
@@ -766,6 +767,7 @@ pub struct DocIndexTemplate {
766 767 #[template(path = "pages/pricing.html")]
767 768 pub struct PricingTemplate {
768 769 pub csrf_token: CsrfTokenOption,
770 + pub tier_prices: crate::tier_prices::TierPrices,
769 771 }
770 772
771 773 /// Use cases page showcasing creator types.
@@ -774,6 +776,7 @@ pub struct PricingTemplate {
774 776 pub struct UseCasesTemplate {
775 777 pub csrf_token: CsrfTokenOption,
776 778 pub session_user: Option<SessionUser>,
779 + pub tier_prices: crate::tier_prices::TierPrices,
777 780 }
778 781
779 782 /// Team page listing the founder, residents, and fellows.
@@ -806,6 +809,7 @@ pub struct CreatorsTemplate {
806 809 pub total_creators: u32,
807 810 pub waitlist_pending: u32,
808 811 pub is_creator: bool,
812 + pub tier_prices: crate::tier_prices::TierPrices,
809 813 }
810 814
811 815 // ============================================================================
@@ -0,0 +1,190 @@
1 + //! Tier prices and storage envelopes pulled from `assumptions.toml` at startup.
2 + //!
3 + //! Templates referencing these via `{{ tier_prices.basic_std }}` etc. stay in
4 + //! sync with the docengine substitution system — both read from the same toml.
5 + //! A price change is a one-line edit to assumptions.toml + a server restart.
6 + //!
7 + //! Missing or wrong-typed keys panic at startup (same pattern as
8 + //! `Assumptions::validate` failure in main.rs). Production never serves with a
9 + //! half-loaded `TierPrices`.
10 +
11 + use docengine::{Assumptions, LookupValue};
12 +
13 + #[derive(Clone, Debug)]
14 + pub struct TierPrices {
15 + // Standard monthly (post-founder sticker rates).
16 + pub basic_std: i32,
17 + pub small_files_std: i32,
18 + pub big_files_std: i32,
19 + pub everything_std: i32,
20 + // Founder monthly (50% of standard, locked for life when window closes).
21 + pub basic_founder: i32,
22 + pub small_files_founder: i32,
23 + pub big_files_founder: i32,
24 + pub everything_founder: i32,
25 + // Standard annual (monthly × 12 × annual_discount.multiplier, rounded).
26 + pub annual_basic_std: i32,
27 + pub annual_small_files_std: i32,
28 + pub annual_big_files_std: i32,
29 + pub annual_everything_std: i32,
30 + // Founder annual.
31 + pub annual_basic_founder: i32,
32 + pub annual_small_files_founder: i32,
33 + pub annual_big_files_founder: i32,
34 + pub annual_everything_founder: i32,
35 + // Per-file caps and total storage caps, as display strings ("10MB", "50GB").
36 + pub basic_per_file: String,
37 + pub small_files_per_file: String,
38 + pub big_files_per_file: String,
39 + pub everything_per_file: String,
40 + pub basic_total: String,
41 + pub small_files_total: String,
42 + pub big_files_total: String,
43 + pub everything_total: String,
44 + // Founder cohort cap, display string with thousands separator ("1,000").
45 + pub cohort_cap_display: String,
46 + }
47 +
48 + impl TierPrices {
49 + pub fn from_assumptions(a: &Assumptions) -> Self {
50 + Self {
51 + basic_std: int_at(a, "tiers.standard.basic"),
52 + small_files_std: int_at(a, "tiers.standard.small_files"),
53 + big_files_std: int_at(a, "tiers.standard.big_files"),
54 + everything_std: int_at(a, "tiers.standard.everything"),
55 + basic_founder: int_at(a, "tiers.founding.basic"),
56 + small_files_founder: int_at(a, "tiers.founding.small_files"),
57 + big_files_founder: int_at(a, "tiers.founding.big_files"),
58 + everything_founder: int_at(a, "tiers.founding.everything"),
59 + annual_basic_std: int_at(a, "derived.annual_standard_basic"),
60 + annual_small_files_std: int_at(a, "derived.annual_standard_small_files"),
61 + annual_big_files_std: int_at(a, "derived.annual_standard_big_files"),
62 + annual_everything_std: int_at(a, "derived.annual_standard_everything"),
63 + annual_basic_founder: int_at(a, "derived.annual_founding_basic"),
64 + annual_small_files_founder: int_at(a, "derived.annual_founding_small_files"),
65 + annual_big_files_founder: int_at(a, "derived.annual_founding_big_files"),
66 + annual_everything_founder: int_at(a, "derived.annual_founding_everything"),
67 + basic_per_file: str_at(a, "tier_limits.basic_per_file"),
68 + small_files_per_file: str_at(a, "tier_limits.small_files_per_file"),
69 + big_files_per_file: str_at(a, "tier_limits.big_files_per_file"),
70 + everything_per_file: str_at(a, "tier_limits.everything_per_file"),
71 + basic_total: str_at(a, "tier_limits.basic_total"),
72 + small_files_total: str_at(a, "tier_limits.small_files_total"),
73 + big_files_total: str_at(a, "tier_limits.big_files_total"),
74 + everything_total: str_at(a, "tier_limits.everything_total"),
75 + cohort_cap_display: str_at(a, "cohort.cap_display"),
76 + }
77 + }
78 + }
79 +
80 + /// Display row for the dashboard tier-picker grid (`user_creator.html`).
81 + #[derive(Clone, Debug)]
82 + pub struct TierCard {
83 + pub key: &'static str,
84 + pub label: &'static str,
85 + pub storage: String,
86 + pub founder_monthly: i32,
87 + pub standard_monthly: i32,
88 + pub founder_annual: i32,
89 + pub standard_annual: i32,
90 + }
91 +
92 + impl TierPrices {
93 + /// Build the four tier cards the dashboard renders. Order matters
94 + /// (Basic, Small Files, Big Files, Everything) — it's the canonical
95 + /// presentation order.
96 + pub fn cards(&self) -> Vec<TierCard> {
97 + vec![
98 + TierCard {
99 + key: "basic",
100 + label: "Basic",
101 + storage: format!("{}, {}/file", self.basic_total, self.basic_per_file),
102 + founder_monthly: self.basic_founder,
103 + standard_monthly: self.basic_std,
104 + founder_annual: self.annual_basic_founder,
105 + standard_annual: self.annual_basic_std,
106 + },
107 + TierCard {
108 + key: "small_files",
109 + label: "Small Files",
110 + storage: format!("{}, {}/file", self.small_files_total, self.small_files_per_file),
111 + founder_monthly: self.small_files_founder,
112 + standard_monthly: self.small_files_std,
113 + founder_annual: self.annual_small_files_founder,
114 + standard_annual: self.annual_small_files_std,
115 + },
116 + TierCard {
117 + key: "big_files",
118 + label: "Big Files",
119 + storage: format!("{}, {}/file", self.big_files_total, self.big_files_per_file),
120 + founder_monthly: self.big_files_founder,
121 + standard_monthly: self.big_files_std,
122 + founder_annual: self.annual_big_files_founder,
123 + standard_annual: self.annual_big_files_std,
124 + },
125 + TierCard {
126 + key: "everything",
127 + label: "Everything",
128 + storage: format!(
129 + "{}, {}/file, all features",
130 + self.everything_total, self.everything_per_file
131 + ),
132 + founder_monthly: self.everything_founder,
133 + standard_monthly: self.everything_std,
134 + founder_annual: self.annual_everything_founder,
135 + standard_annual: self.annual_everything_std,
136 + },
137 + ]
138 + }
139 + }
140 +
141 + fn int_at(a: &Assumptions, key: &str) -> i32 {
142 + match a.get(key) {
143 + Some(LookupValue::Int(n)) => i32::try_from(*n)
144 + .unwrap_or_else(|_| panic!("{key} = {n} does not fit in i32")),
145 + Some(LookupValue::Float(x)) => x.round() as i32,
146 + other => panic!("expected integer at {key}, got {other:?}"),
147 + }
148 + }
149 +
150 + fn str_at(a: &Assumptions, key: &str) -> String {
151 + match a.get(key) {
152 + Some(LookupValue::String(s)) => s.clone(),
153 + other => panic!("expected string at {key}, got {other:?}"),
154 + }
155 + }
156 +
157 + #[cfg(test)]
158 + mod tests {
159 + use super::*;
160 +
161 + const ASSUMPTIONS_PATH: &str =
162 + "../../_private/docs/mnw/server-internal/business/assumptions.toml";
163 +
164 + /// Guards every key TierPrices reads. If a future toml edit removes one of
165 + /// these or flips its type, the panic in `from_assumptions` will fire at
166 + /// startup; this test catches it at PR time instead.
167 + #[test]
168 + fn from_canonical_assumptions_populates_every_field() {
169 + let a = Assumptions::load(ASSUMPTIONS_PATH).expect("load canonical toml");
170 + let p = TierPrices::from_assumptions(&a);
171 +
172 + // Sanity-check sentinels (values match the toml; if they change in
173 + // toml, update here too).
174 + assert_eq!(p.basic_std, 16);
175 + assert_eq!(p.everything_std, 60);
176 + assert_eq!(p.basic_founder, 8);
177 + assert_eq!(p.annual_basic_std, 173);
178 + assert_eq!(p.annual_everything_founder, 324);
179 + assert_eq!(p.basic_per_file, "10MB");
180 + assert_eq!(p.everything_total, "500GB");
181 + assert_eq!(p.cohort_cap_display, "1,000");
182 +
183 + // Cards iteration produces the four canonical rows.
184 + let cards = p.cards();
185 + assert_eq!(cards.len(), 4);
186 + assert_eq!(cards[0].key, "basic");
187 + assert_eq!(cards[3].key, "everything");
188 + assert_eq!(cards[1].standard_monthly, 24);
189 + }
190 + }
@@ -57,27 +57,27 @@
57 57 <tbody>
58 58 <tr>
59 59 <td>Basic</td>
60 - <td>$16</td>
60 + <td>${{ tier_prices.basic_std }}</td>
61 61 <td>Text, blogs, newsletters</td>
62 - <td>50 GB</td>
62 + <td>{{ tier_prices.basic_total }}</td>
63 63 </tr>
64 64 <tr>
65 65 <td>Small Files</td>
66 - <td>$24</td>
66 + <td>${{ tier_prices.small_files_std }}</td>
67 67 <td>Audio, plugins, small software</td>
68 - <td>250 GB</td>
68 + <td>{{ tier_prices.small_files_total }}</td>
69 69 </tr>
70 70 <tr>
71 71 <td>Big Files</td>
72 - <td>$36</td>
72 + <td>${{ tier_prices.big_files_std }}</td>
73 73 <td>Video, games, large software</td>
74 - <td>500 GB</td>
74 + <td>{{ tier_prices.big_files_total }}</td>
75 75 </tr>
76 76 <tr>
77 77 <td>Everything</td>
78 - <td>$60</td>
78 + <td>${{ tier_prices.everything_std }}</td>
79 79 <td>All features, current and future</td>
80 - <td>500 GB</td>
80 + <td>{{ tier_prices.everything_total }}</td>
81 81 </tr>
82 82 </tbody>
83 83 </table>
@@ -37,19 +37,19 @@
37 37 <div class="founder-tier-grid" aria-label="Founder tier pricing">
38 38 <div class="founder-tier">
39 39 <span class="founder-tier-name">Basic</span>
40 - <span class="founder-tier-price"><del>$16</del> <strong>$8</strong><span class="founder-tier-unit">/mo</span></span>
40 + <span class="founder-tier-price"><del>${{ tier_prices.basic_std }}</del> <strong>${{ tier_prices.basic_founder }}</strong><span class="founder-tier-unit">/mo</span></span>
41 41 </div>
42 42 <div class="founder-tier">
43 43 <span class="founder-tier-name">Small Files</span>
44 - <span class="founder-tier-price"><del>$24</del> <strong>$12</strong><span class="founder-tier-unit">/mo</span></span>
44 + <span class="founder-tier-price"><del>${{ tier_prices.small_files_std }}</del> <strong>${{ tier_prices.small_files_founder }}</strong><span class="founder-tier-unit">/mo</span></span>
45 45 </div>
46 46 <div class="founder-tier">
47 47 <span class="founder-tier-name">Big Files</span>
48 - <span class="founder-tier-price"><del>$36</del> <strong>$18</strong><span class="founder-tier-unit">/mo</span></span>
48 + <span class="founder-tier-price"><del>${{ tier_prices.big_files_std }}</del> <strong>${{ tier_prices.big_files_founder }}</strong><span class="founder-tier-unit">/mo</span></span>
49 49 </div>
50 50 <div class="founder-tier">
51 51 <span class="founder-tier-name">Everything</span>
52 - <span class="founder-tier-price"><del>$60</del> <strong>$30</strong><span class="founder-tier-unit">/mo</span></span>
52 + <span class="founder-tier-price"><del>${{ tier_prices.everything_std }}</del> <strong>${{ tier_prices.everything_founder }}</strong><span class="founder-tier-unit">/mo</span></span>
53 53 </div>
54 54 </div>
55 55 {% if let Some(remaining) = founder_slots_remaining %}
@@ -67,7 +67,7 @@
67 67
68 68 <ul class="fork-list">
69 69 {% if !founder_window_open %}
70 - <li><strong>Flat monthly pricing</strong>: starts at $16/mo</li>
70 + <li><strong>Flat monthly pricing</strong>: starts at ${{ tier_prices.basic_std }}/mo</li>
71 71 {% endif %}
72 72 <li><strong>0% platform fee</strong>: only ~3% payment processing</li>
73 73 <li><strong>Every tier is the complete platform</strong>: profile, forum, discovery, memberships. Tier picks the file-size envelope, not the feature set</li>
@@ -112,7 +112,7 @@
112 112 <ul class="how-list">
113 113 <li><strong>No investors, no ads</strong>: funded by subscriptions, not surveillance.</li>
114 114 <li><strong>No tracking</strong>: no banners, no pixels, no sponsored placements.</li>
115 - <li><strong>Cheaper at scale</strong>: at $50k/mo, a 10% platform takes $5,000. We take a flat tier fee ($16&ndash;$60).</li>
115 + <li><strong>Cheaper at scale</strong>: at $50k/mo, a 10% platform takes $5,000. We take a flat tier fee (${{ tier_prices.basic_std }}&ndash;${{ tier_prices.everything_std }}).</li>
116 116 <li><strong>Built to last</strong>: no debt, no growth mandates, no reason to enshittify.</li>
117 117 </ul>
118 118 <a class="section-link" href="/docs/how-we-work">How the business model works</a>
@@ -23,27 +23,27 @@
23 23 <p class="tier-intro">Every tier is the complete platform: profile, project pages, forum, discovery, memberships, analytics, full data export. The tier picks the file-size envelope, not the feature set.</p>
24 24 <div class="tier-selector" role="radiogroup" aria-label="Content tier">
25 25 <label class="tier-card tier-option is-selected">
26 - <input type="radio" name="tier" value="16" checked>
26 + <input type="radio" name="tier" value="{{ tier_prices.basic_std }}" checked>
27 27 <div class="tier-name">Basic</div>
28 - <div class="tier-price">$16/mo</div>
29 - <div class="tier-desc">10MB/file, 50GB total. Fits text, blogs, newsletters.</div>
28 + <div class="tier-price">${{ tier_prices.basic_std }}/mo</div>
29 + <div class="tier-desc">{{ tier_prices.basic_per_file }}/file, {{ tier_prices.basic_total }} total. Fits text, blogs, newsletters.</div>
30 30 </label>
31 31 <label class="tier-card tier-option">
32 - <input type="radio" name="tier" value="24">
32 + <input type="radio" name="tier" value="{{ tier_prices.small_files_std }}">
33 33 <div class="tier-name">Small Files</div>
34 - <div class="tier-price">$24/mo</div>
35 - <div class="tier-desc">500MB/file, 250GB total. Fits audio, plugins, binaries.</div>
34 + <div class="tier-price">${{ tier_prices.small_files_std }}/mo</div>
35 + <div class="tier-desc">{{ tier_prices.small_files_per_file }}/file, {{ tier_prices.small_files_total }} total. Fits audio, plugins, binaries.</div>
36 36 </label>
37 37 <label class="tier-card tier-option">
38 - <input type="radio" name="tier" value="36">
38 + <input type="radio" name="tier" value="{{ tier_prices.big_files_std }}">
39 39 <div class="tier-name">Big Files</div>
40 - <div class="tier-price">$36/mo</div>
41 - <div class="tier-desc">20GB/file, 500GB total. Fits video, games, large software.</div>
40 + <div class="tier-price">${{ tier_prices.big_files_std }}/mo</div>
41 + <div class="tier-desc">{{ tier_prices.big_files_per_file }}/file, {{ tier_prices.big_files_total }} total. Fits video, games, large software.</div>
42 42 </label>
43 43 <label class="tier-card tier-option">
44 - <input type="radio" name="tier" value="60">
44 + <input type="radio" name="tier" value="{{ tier_prices.everything_std }}">
45 45 <div class="tier-name">Everything</div>
46 - <div class="tier-price">$60/mo</div>
46 + <div class="tier-price">${{ tier_prices.everything_std }}/mo</div>
47 47 <div class="tier-desc">Big Files envelope plus first access to high-cost features as they ship.</div>
48 48 </label>
49 49 </div>
@@ -53,7 +53,7 @@
53 53 <h2 class="section-label">You keep</h2>
54 54 <div class="mnw-summary">
55 55 <div class="mnw-keep" id="mnw-keep">$943.00</div>
56 - <div class="mnw-detail" id="mnw-detail">of every $1,000 after processing fees (~3%) and $16/mo membership</div>
56 + <div class="mnw-detail" id="mnw-detail">of every $1,000 after processing fees (~3%) and ${{ tier_prices.basic_std }}/mo membership</div>
57 57 </div>
58 58 </div>
59 59
@@ -83,10 +83,10 @@
83 83 <h2 class="section-label">Where your tier fee goes</h2>
84 84 <p class="cost-allocation-intro">Per typical creator, monthly. These are platform-wide budget allocations, not per-account accounting. Hover a segment for details.</p>
85 85
86 - {# Basic — $16 = 0.76 + 0.40 + 5.00 + 6.00 + 2.00 + 1.84 #}
86 + {# Basic — $16 = 0.76 + 0.40 + 5.00 + 6.00 + 2.00 + 1.84. Sub-amounts hardcoded; see launchplan §4.5.1 for the planned [cost_allocation] toml section. #}
87 87 <div class="cost-row">
88 - <div class="cost-row-label"><strong>Basic</strong>$16/mo</div>
89 - <div class="cost-bar" role="img" aria-label="Basic tier $16/mo allocation: Stripe $0.76, Storage $0.40, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $1.84">
88 + <div class="cost-row-label"><strong>Basic</strong>${{ tier_prices.basic_std }}/mo</div>
89 + <div class="cost-bar" role="img" aria-label="Basic tier ${{ tier_prices.basic_std }}/mo allocation: Stripe $0.76, Storage $0.40, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $1.84">
90 90 <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 76" title="Stripe processing: $0.76. Stripe's fee on your monthly subscription.">$0.76</div>
91 91 <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 40" title="Storage: $0.40. Object storage at your tier's typical fill, on Hetzner.">$0.40</div>
92 92 <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00. Identity recovery, billing disputes, moderation, abuse, and legal — the work that can't be automated.">$5.00</div>
@@ -98,8 +98,8 @@
98 98
99 99 {# Small Files — $24 = 1.00 + 0.60 + 5.00 + 6.00 + 2.00 + 9.40 #}
100 100 <div class="cost-row">
101 - <div class="cost-row-label"><strong>Small Files</strong>$24/mo</div>
102 - <div class="cost-bar" role="img" aria-label="Small Files tier $24/mo allocation: Stripe $1.00, Storage $0.60, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $9.40">
101 + <div class="cost-row-label"><strong>Small Files</strong>${{ tier_prices.small_files_std }}/mo</div>
102 + <div class="cost-bar" role="img" aria-label="Small Files tier ${{ tier_prices.small_files_std }}/mo allocation: Stripe $1.00, Storage $0.60, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $9.40">
103 103 <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 100" title="Stripe processing: $1.00.">$1.00</div>
104 104 <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 60" title="Storage: $0.60. Object storage at your tier's typical fill.">$0.60</div>
105 105 <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00.">$5.00</div>
@@ -111,8 +111,8 @@
111 111
112 112 {# Big Files — $36 = 1.34 + 0.90 + 5.00 + 6.00 + 2.00 + 20.76 #}
113 113 <div class="cost-row">
114 - <div class="cost-row-label"><strong>Big Files</strong>$36/mo</div>
115 - <div class="cost-bar" role="img" aria-label="Big Files tier $36/mo allocation: Stripe $1.34, Storage $0.90, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $20.76">
114 + <div class="cost-row-label"><strong>Big Files</strong>${{ tier_prices.big_files_std }}/mo</div>
115 + <div class="cost-bar" role="img" aria-label="Big Files tier ${{ tier_prices.big_files_std }}/mo allocation: Stripe $1.34, Storage $0.90, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $20.76">
116 116 <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 134" title="Stripe processing: $1.34.">$1.34</div>
117 117 <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 90" title="Storage: $0.90.">$0.90</div>
118 118 <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00.">$5.00</div>
@@ -124,8 +124,8 @@
124 124
125 125 {# Everything — $60 = 2.04 + 1.50 + 5.00 + 6.00 + 2.00 + 43.46 #}
126 126 <div class="cost-row">
127 - <div class="cost-row-label"><strong>Everything</strong>$60/mo</div>
128 - <div class="cost-bar" role="img" aria-label="Everything tier $60/mo allocation: Stripe $2.04, Storage $1.50, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $43.46">
127 + <div class="cost-row-label"><strong>Everything</strong>${{ tier_prices.everything_std }}/mo</div>
128 + <div class="cost-bar" role="img" aria-label="Everything tier ${{ tier_prices.everything_std }}/mo allocation: Stripe $2.04, Storage $1.50, Support $5.00, Engineering $6.00, Reserves $2.00, Earn-back $43.46">
129 129 <div class="cost-bar-seg cost-bar-seg-stripe" style="flex: 204" title="Stripe processing: $2.04.">$2.04</div>
130 130 <div class="cost-bar-seg cost-bar-seg-storage" style="flex: 150" title="Storage: $1.50.">$1.50</div>
131 131 <div class="cost-bar-seg cost-bar-seg-support" style="flex: 500" title="Human support time: $5.00.">$5.00</div>
@@ -291,7 +291,8 @@
291 291 var revenue = parseFloat(revenueInput.value) || 0;
292 292
293 293 var tierRadios = document.querySelectorAll('input[name="tier"]');
294 - var tierCost = 16;
294 + // Default falls back to the Basic-tier radio value; templates render the canonical price.
295 + var tierCost = {{ tier_prices.basic_std }};
295 296 for (var i = 0; i < tierRadios.length; i++) {
296 297 if (tierRadios[i].checked) {
297 298 tierCost = parseInt(tierRadios[i].value, 10);
@@ -24,7 +24,7 @@
24 24 <li>RSS feed per project</li>
25 25 <li>Pay-what-you-want pricing</li>
26 26 </ul>
27 - <div class="use-case-tier">Small Files $24/mo &middot; 250GB, 500MB/file</div>
27 + <div class="use-case-tier">Small Files ${{ tier_prices.small_files_std }}/mo &middot; {{ tier_prices.small_files_total }}, {{ tier_prices.small_files_per_file }}/file</div>
28 28 </div>
29 29
30 30 <div class="use-case-card">
@@ -37,7 +37,7 @@
37 37 <li>Subscriber-only feeds</li>
38 38 <li>Broadcast emails to followers</li>
39 39 </ul>
40 - <div class="use-case-tier">Small Files $24/mo &middot; 250GB, 500MB/file</div>
40 + <div class="use-case-tier">Small Files ${{ tier_prices.small_files_std }}/mo &middot; {{ tier_prices.small_files_total }}, {{ tier_prices.small_files_per_file }}/file</div>
41 41 </div>
42 42
43 43 <div class="use-case-card">
@@ -51,7 +51,7 @@
51 51 <li>Scheduled publishing</li>
52 52 <li>Broadcast emails</li>
53 53 </ul>
54 - <div class="use-case-tier">Basic $16/mo &middot; 50GB, 10MB/file</div>
54 + <div class="use-case-tier">Basic ${{ tier_prices.basic_std }}/mo &middot; {{ tier_prices.basic_total }}, {{ tier_prices.basic_per_file }}/file</div>
55 55 </div>
56 56
57 57 <div class="use-case-card">
@@ -64,7 +64,7 @@
64 64 <li>Git source browser</li>
65 65 <li>Promo codes and discounts</li>
66 66 </ul>
67 - <div class="use-case-tier">Small Files $24/mo &middot; 250GB, 500MB/file</div>
67 + <div class="use-case-tier">Small Files ${{ tier_prices.small_files_std }}/mo &middot; {{ tier_prices.small_files_total }}, {{ tier_prices.small_files_per_file }}/file</div>
68 68 </div>
69 69
70 70 <div class="use-case-card">
@@ -78,7 +78,7 @@
78 78 <li>Pay-what-you-want pricing</li>
79 79 <li>Cover art</li>
80 80 </ul>
81 - <div class="use-case-tier">Small Files $24/mo &middot; 250GB, 500MB/file</div>
81 + <div class="use-case-tier">Small Files ${{ tier_prices.small_files_std }}/mo &middot; {{ tier_prices.small_files_total }}, {{ tier_prices.small_files_per_file }}/file</div>
82 82 </div>
83 83
84 84 <div class="use-case-card">
@@ -91,7 +91,7 @@
91 91 <li>Subscription tiers</li>
92 92 <li>Cover images</li>
93 93 </ul>
94 - <div class="use-case-tier">Small Files $24/mo &middot; 250GB, 500MB/file</div>
94 + <div class="use-case-tier">Small Files ${{ tier_prices.small_files_std }}/mo &middot; {{ tier_prices.small_files_total }}, {{ tier_prices.small_files_per_file }}/file</div>
95 95 </div>
96 96
97 97 <div class="use-case-card">
@@ -104,7 +104,7 @@
104 104 <li>Subscription tiers</li>
105 105 <li>Scheduled publishing</li>
106 106 </ul>
107 - <div class="use-case-tier">Basic $16/mo or Small Files $24/mo</div>
107 + <div class="use-case-tier">Basic ${{ tier_prices.basic_std }}/mo or Small Files ${{ tier_prices.small_files_std }}/mo</div>
108 108 </div>
109 109
110 110 <div class="use-case-card">
@@ -117,7 +117,7 @@
117 117 <li>License keys with activation limits</li>
118 118 <li>Promo codes</li>
119 119 </ul>
120 - <div class="use-case-tier">Big Files $36/mo &middot; 500GB, 20GB/file</div>
120 + <div class="use-case-tier">Big Files ${{ tier_prices.big_files_std }}/mo &middot; {{ tier_prices.big_files_total }}, {{ tier_prices.big_files_per_file }}/file</div>
121 121 </div>
122 122
123 123 <div class="use-case-card">
@@ -130,7 +130,7 @@
130 130 <li>Subscriptions</li>
131 131 <li>Scheduled publishing</li>
132 132 </ul>
133 - <div class="use-case-tier">Small Files $24/mo &middot; 250GB, 500MB/file</div>
133 + <div class="use-case-tier">Small Files ${{ tier_prices.small_files_std }}/mo &middot; {{ tier_prices.small_files_total }}, {{ tier_prices.small_files_per_file }}/file</div>
134 134 </div>
135 135 </div>
136 136 </div>
@@ -147,7 +147,7 @@
147 147 <li>Adaptive streaming</li>
148 148 <li>All existing features included</li>
149 149 </ul>
150 - <div class="use-case-tier">Big Files $36/mo &middot; 500GB, 20GB/file</div>
150 + <div class="use-case-tier">Big Files ${{ tier_prices.big_files_std }}/mo &middot; {{ tier_prices.big_files_total }}, {{ tier_prices.big_files_per_file }}/file</div>
151 151 </div>
152 152
153 153 <div class="use-case-card coming-soon">
@@ -159,7 +159,7 @@
159 159 <li>VOD archives</li>
160 160 <li>All existing features included</li>
161 161 </ul>
162 - <div class="use-case-tier">Everything $60/mo &middot; 500GB, all features</div>
162 + <div class="use-case-tier">Everything ${{ tier_prices.everything_std }}/mo &middot; {{ tier_prices.everything_total }}, all features</div>
163 163 </div>
164 164 </div>
165 165 </div>
@@ -53,39 +53,27 @@
53 53 {% if creator_tiers_configured %}
54 54 {% let show_founder_rate = founder_window_open || is_founder_locked %}
55 55 <div class="creator-tier-grid mb-4">
56 - {% for card in [
57 - ("basic", "Basic", "50GB, 10MB/file", 5, 10, 54, 108),
58 - ("small_files", "Small Files", "250GB, 500MB/file", 10, 20, 108, 216),
59 - ("big_files", "Big Files", "500GB, 20GB/file", 15, 30, 162, 324),
60 - ("everything", "Everything", "500GB, 20GB/file, all features", 30, 60, 324, 648),
61 - ] %}
62 - {% let tier_key = card.0 %}
63 - {% let tier_label = card.1 %}
64 - {% let tier_storage = card.2 %}
65 - {% let founder_monthly = card.3 %}
66 - {% let sticker_monthly = card.4 %}
67 - {% let founder_annual = card.5 %}
68 - {% let sticker_annual = card.6 %}
56 + {% for card in tier_cards %}
69 57 <div class="creator-tier-card">
70 - <div class="creator-tier-name"><a href="/docs/guide/tiers" class="creator-tier-link">{{ tier_label }}</a></div>
58 + <div class="creator-tier-name"><a href="/docs/guide/tiers" class="creator-tier-link">{{ card.label }}</a></div>
71 59 {% if show_founder_rate %}
72 - <div class="meta mb-2"><strong>${{ founder_monthly }}/mo</strong> <span class="strike-dim">${{ sticker_monthly }}</span></div>
73 - <div class="meta creator-tier-sub mb-2">or ${{ founder_annual }}/yr (10% off)</div>
60 + <div class="meta mb-2"><strong>${{ card.founder_monthly }}/mo</strong> <span class="strike-dim">${{ card.standard_monthly }}</span></div>
61 + <div class="meta creator-tier-sub mb-2">or ${{ card.founder_annual }}/yr (10% off)</div>
74 62 {% else %}
75 - <div class="meta mb-2">${{ sticker_monthly }}/mo</div>
76 - <div class="meta creator-tier-sub mb-2">or ${{ sticker_annual }}/yr (10% off)</div>
63 + <div class="meta mb-2">${{ card.standard_monthly }}/mo</div>
64 + <div class="meta creator-tier-sub mb-2">or ${{ card.standard_annual }}/yr (10% off)</div>
77 65 {% endif %}
78 - <div class="meta creator-tier-storage mb-3">{{ tier_storage }}</div>
66 + <div class="meta creator-tier-storage mb-3">{{ card.storage }}</div>
79 67 <div class="creator-tier-buttons">
80 68 <form method="post" action="/stripe/creator-tier" class="m-0">
81 69 {% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %}
82 - <input type="hidden" name="tier" value="{{ tier_key }}">
70 + <input type="hidden" name="tier" value="{{ card.key }}">
83 71 <input type="hidden" name="interval" value="monthly">
84 72 <button type="submit" class="btn-primary creator-tier-btn" data-loading-text="Redirecting to Stripe...">Monthly</button>
85 73 </form>
86 74 <form method="post" action="/stripe/creator-tier" class="m-0">
87 75 {% if let Some(token) = csrf_token %}<input type="hidden" name="_csrf" value="{{ token }}">{% endif %}
88 - <input type="hidden" name="tier" value="{{ tier_key }}">
76 + <input type="hidden" name="tier" value="{{ card.key }}">
89 77 <input type="hidden" name="interval" value="annual">
90 78 <button type="submit" class="btn-secondary creator-tier-btn" data-loading-text="Redirecting to Stripe...">Annual (save 10%)</button>
91 79 </form>
@@ -273,10 +261,10 @@
273 261 <label for="preferred-tier">Which tier fits?</label>
274 262 <select id="preferred-tier" name="preferred_tier">
275 263 <option value="">Not sure yet</option>
276 - <option value="basic">Basic ($16/mo): text, blogs, newsletters</option>
277 - <option value="small_files">Small Files ($24/mo): audio, software, plugins</option>
278 - <option value="big_files">Big Files ($36/mo): video, games, large software</option>
279 - <option value="everything">Everything ($60/mo): all features, current and future</option>
264 + <option value="basic">Basic (${{ tier_cards[0].standard_monthly }}/mo): text, blogs, newsletters</option>
265 + <option value="small_files">Small Files (${{ tier_cards[1].standard_monthly }}/mo): audio, software, plugins</option>
266 + <option value="big_files">Big Files (${{ tier_cards[2].standard_monthly }}/mo): video, games, large software</option>
267 + <option value="everything">Everything (${{ tier_cards[3].standard_monthly }}/mo): all features, current and future</option>
280 268 </select>
281 269 </div>
282 270 <div class="form-group">