Skip to content

Commit f4b4734

Browse files
authored
Merge pull request #221 from ddxv/main
Normalized Subscription Tables
2 parents 34f64a6 + f2188d9 commit f4b4734

26 files changed

Lines changed: 583 additions & 835 deletions

File tree

backend/api_app/guards.py

Lines changed: 14 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -41,70 +41,12 @@ class TierLimits:
4141

4242
TIER_LIMITS: dict[str, TierLimits] = {**PUBLIC_TIER_LIMITS, **LEGACY_TIER_LIMITS}
4343

44-
REQUIRED_PRICE_MAPPED_TIERS = frozenset(
45-
tier for tier in PUBLIC_TIER_LIMITS if tier != "free"
46-
)
47-
48-
PRICE_ID_TO_TIER: dict[str, str] = {}
49-
50-
51-
def validate_tier_mapping_config(price_map: dict[str, str] | None) -> None:
52-
"""Validate the required Stripe price to tier mapping config."""
53-
if not price_map:
54-
raise ValueError(
55-
"Missing required [tier_prices] section in config.toml. "
56-
f"Expected mappings for: {', '.join(sorted(REQUIRED_PRICE_MAPPED_TIERS))}"
57-
)
58-
59-
configured_tiers = set(price_map.values())
60-
unknown_tiers = sorted(configured_tiers - set(TIER_LIMITS))
61-
if unknown_tiers:
62-
raise ValueError(
63-
"Unknown tier label(s) in [tier_prices]: " + ", ".join(unknown_tiers)
64-
)
65-
66-
missing_tiers = sorted(REQUIRED_PRICE_MAPPED_TIERS - configured_tiers)
67-
if missing_tiers:
68-
raise ValueError(
69-
"Missing required tier mapping(s) in [tier_prices]: "
70-
+ ", ".join(missing_tiers)
71-
)
72-
73-
74-
def configure_tier_mapping(price_map: dict[str, str]) -> None:
75-
"""Call at startup with {stripe_price_id: tier_label} mapping.
76-
77-
This is populated from the SvelteKit stripe.ts constant values so we
78-
don't duplicate them. The keys come from ``STRIPE_PRICES`` in
79-
``frontend/src/lib/server/stripe.ts``.
80-
"""
81-
PRICE_ID_TO_TIER.update(price_map)
82-
8344

8445
def get_tier_limits(tier: str) -> TierLimits:
8546
"""Get limits for a tier, defaulting to free."""
8647
return TIER_LIMITS.get(tier, TIER_LIMITS["free"])
8748

8849

89-
def _resolve_tier(price_id: str | None) -> str:
90-
"""Resolve a subscription price identifier to a configured tier label."""
91-
if not price_id:
92-
return "free"
93-
94-
if price_id in TIER_LIMITS:
95-
return price_id
96-
97-
mapped_tier = PRICE_ID_TO_TIER.get(price_id)
98-
if mapped_tier in TIER_LIMITS:
99-
return mapped_tier
100-
101-
logger.warning(
102-
"Unknown subscription price id %r for API rate limiting; defaulting to free",
103-
price_id,
104-
)
105-
return "free"
106-
107-
10850
# ---------------------------------------------------------------------------
10951
# In-memory rate limiter (refilling token bucket per key)
11052
# ---------------------------------------------------------------------------
@@ -247,26 +189,28 @@ class ApiKeyContext:
247189

248190

249191
def _query_key(engine: Engine, key_hash: str) -> _CachedKey | None:
250-
"""Look up an API key and its owner's subscription tier."""
192+
"""Look up an API key and its owner's subscription tier.
193+
194+
Resolves the tier through the tier_prices → tiers JOIN.
195+
"""
251196
query = text("""
252197
SELECT ak.user_id,
253-
COALESCE(s.provider_price_id, 'free') AS price_id
198+
COALESCE(t.slug, 'free') AS tier
254199
FROM public.api_keys ak
255200
JOIN public.users u ON u.id = ak.user_id
256201
LEFT JOIN LATERAL (
257-
SELECT provider_price_id
202+
SELECT sub.tier_price_id
258203
FROM public.subscriptions sub
259204
WHERE sub.user_id = ak.user_id
260-
AND (
261-
(sub.cancel_at IS NOT NULL AND sub.cancel_at > now())
262-
OR (
263-
sub.cancel_at IS NULL
264-
AND sub.status IN ('active', 'trialing')
265-
)
266-
)
267-
ORDER BY sub.updated_at DESC
205+
AND (
206+
(sub.cancel_at IS NOT NULL AND sub.cancel_at > now())
207+
OR (sub.cancel_at IS NULL AND sub.status IN ('active', 'trialing'))
208+
)
209+
ORDER BY sub.updated_at DESC
268210
LIMIT 1
269211
) s ON TRUE
212+
LEFT JOIN public.tier_prices tp ON tp.id = s.tier_price_id
213+
LEFT JOIN public.tiers t ON t.id = tp.tier_id
270214
WHERE ak.key_hash = :key_hash
271215
AND ak.is_active = true
272216
AND (ak.expires_at IS NULL OR ak.expires_at > now())
@@ -278,8 +222,7 @@ def _query_key(engine: Engine, key_hash: str) -> _CachedKey | None:
278222
if row is None:
279223
return None
280224

281-
price_id = row.price_id or "free"
282-
tier = _resolve_tier(price_id)
225+
tier = row.tier or "free"
283226

284227
return _CachedKey(
285228
user_id=row.user_id,

backend/app.py

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,11 @@
2828
from api_app.controllers.rankings import RankingsController
2929
from api_app.controllers.scry import ScryController
3030
from api_app.controllers.sdks import SdksController
31-
from api_app.guards import configure_tier_mapping, validate_tier_mapping_config
3231
from api_app.mcp.controller import (
3332
fastmcp_asgi_app,
3433
protected_mcp_app,
3534
set_mcp_engine,
3635
)
37-
from config import CONFIG
3836
from dbcon.connections import get_db_connection
3937
from dbcon.static import load_static_data
4038

@@ -198,32 +196,8 @@ async def db_lifespan(app: Litestar) -> AsyncGenerator[None]:
198196
"""
199197
logger.info("Starting database connections...")
200198

201-
# Load Stripe price → tier mapping from config
202-
tier_prices_raw = CONFIG.get("tier_prices")
203-
tier_prices: dict[str, str] | None = None
204-
205-
if tier_prices_raw is None:
206-
tier_prices = None
207-
elif not isinstance(tier_prices_raw, dict) or not all(
208-
isinstance(key, str) and isinstance(value, str)
209-
for key, value in tier_prices_raw.items()
210-
):
211-
logger.exception("Invalid API tier pricing config")
212-
raise RuntimeError("Invalid API tier pricing config in config.toml")
213-
else:
214-
tier_prices = {key: value for key, value in tier_prices_raw.items()}
215-
216-
try:
217-
validate_tier_mapping_config(tier_prices)
218-
except ValueError as exc:
219-
logger.exception("Invalid API tier pricing config")
220-
raise RuntimeError("Invalid API tier pricing config in config.toml") from exc
221-
222-
if tier_prices is None:
223-
raise RuntimeError("Invalid API tier pricing config in config.toml")
224-
225-
configure_tier_mapping(tier_prices)
226-
logger.info(f"Loaded {len(tier_prices)} tier price mappings")
199+
# Tier prices are now resolved at query time through the
200+
# tier_prices → tiers JOIN. No startup config needed.
227201

228202
# Initialize connections
229203
try:

backend/dbcon/sql/query_sitemap_companies.sql

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,5 @@ WHERE
1010
app_count > 2 AND (company_name IS NOT NULL OR installs_d30 > 10000000)
1111
GROUP BY
1212
company_domain,
13-
company_name,
14-
type_url_slug
15-
;
13+
company_name,
14+
type_url_slug;

frontend/appgoblin-theme.css

Lines changed: 72 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
--text-scaling: 1;
33
--base-font-color: oklch(0 0 0 / 1);
44
--base-font-color-dark: oklch(1 0 0 / 1);
5-
--base-font-family: system-ui, sans-serif;
5+
--base-font-family: inherit;
66
--base-font-size: inherit;
77
--base-line-height: inherit;
88
--base-font-weight: normal;
@@ -34,17 +34,17 @@
3434
--default-ring-width: 1px;
3535
--body-background-color: var(--color-surface-50);
3636
--body-background-color-dark: var(--color-surface-950);
37-
--color-primary-50: oklch(97.01% 0.05 150.78deg);
38-
--color-primary-100: oklch(93.83% 0.06 150.79deg);
39-
--color-primary-200: oklch(90.67% 0.07 150.67deg);
40-
--color-primary-300: oklch(87.34% 0.09 149.61deg);
41-
--color-primary-400: oklch(84.21% 0.1 149.47deg);
42-
--color-primary-500: oklch(81.1% 0.11 149.23deg);
43-
--color-primary-600: oklch(73.64% 0.11 150.01deg);
44-
--color-primary-700: oklch(65.99% 0.12 150.89deg);
45-
--color-primary-800: oklch(58.24% 0.11 151.21deg);
46-
--color-primary-900: oklch(50.33% 0.11 151.24deg);
47-
--color-primary-950: oklch(42.35% 0.11 150.11deg);
37+
--color-primary-50: oklch(97.62% 0.04 150deg);
38+
--color-primary-100: oklch(94.33% 0.05 150.14deg);
39+
--color-primary-200: oklch(90.85% 0.07 150.23deg);
40+
--color-primary-300: oklch(87.65% 0.08 149.54deg);
41+
--color-primary-400: oklch(84.2% 0.09 149.45deg);
42+
--color-primary-500: oklch(81% 0.11 149.14deg);
43+
--color-primary-600: oklch(71.08% 0.1 149.79deg);
44+
--color-primary-700: oklch(61.08% 0.1 150.32deg);
45+
--color-primary-800: oklch(50.36% 0.09 150.69deg);
46+
--color-primary-900: oklch(39.51% 0.09 150.64deg);
47+
--color-primary-950: oklch(27.72% 0.08 150.03deg);
4848
--color-primary-contrast-dark: var(--color-primary-950);
4949
--color-primary-contrast-light: var(--color-primary-50);
5050
--color-primary-contrast-50: var(--color-primary-contrast-dark);
@@ -54,21 +54,21 @@
5454
--color-primary-contrast-400: var(--color-primary-contrast-dark);
5555
--color-primary-contrast-500: var(--color-primary-contrast-dark);
5656
--color-primary-contrast-600: var(--color-primary-contrast-dark);
57-
--color-primary-contrast-700: var(--color-primary-contrast-light);
57+
--color-primary-contrast-700: var(--color-primary-contrast-dark);
5858
--color-primary-contrast-800: var(--color-primary-contrast-light);
5959
--color-primary-contrast-900: var(--color-primary-contrast-light);
6060
--color-primary-contrast-950: var(--color-primary-contrast-light);
6161
--color-secondary-50: oklch(93.45% 0.04 306.54deg);
62-
--color-secondary-100: oklch(90.54% 0.05 306.71deg);
62+
--color-secondary-100: oklch(90.58% 0.06 306.17deg);
6363
--color-secondary-200: oklch(87.59% 0.07 305.38deg);
64-
--color-secondary-300: oklch(84.51% 0.09 305.86deg);
64+
--color-secondary-300: oklch(84.55% 0.09 305.51deg);
6565
--color-secondary-400: oklch(81.58% 0.1 304.84deg);
66-
--color-secondary-500: oklch(78.71% 0.12 304.79deg);
67-
--color-secondary-600: oklch(69.5% 0.11 304.15deg);
68-
--color-secondary-700: oklch(60.2% 0.1 304.04deg);
69-
--color-secondary-800: oklch(50.3% 0.09 303.05deg);
70-
--color-secondary-900: oklch(40.15% 0.08 302.59deg);
71-
--color-secondary-950: oklch(29.1% 0.08 300.46deg);
66+
--color-secondary-500: oklch(78.76% 0.12 304.51deg);
67+
--color-secondary-600: oklch(69.54% 0.11 303.84deg);
68+
--color-secondary-700: oklch(60.12% 0.11 303.7deg);
69+
--color-secondary-800: oklch(50.35% 0.1 302.66deg);
70+
--color-secondary-900: oklch(40.08% 0.09 302.12deg);
71+
--color-secondary-950: oklch(29.05% 0.08 299.88deg);
7272
--color-secondary-contrast-dark: var(--color-secondary-950);
7373
--color-secondary-contrast-light: var(--color-secondary-50);
7474
--color-secondary-contrast-50: var(--color-secondary-contrast-dark);
@@ -82,15 +82,15 @@
8282
--color-secondary-contrast-800: var(--color-secondary-contrast-light);
8383
--color-secondary-contrast-900: var(--color-secondary-contrast-light);
8484
--color-secondary-contrast-950: var(--color-secondary-contrast-light);
85-
--color-tertiary-50: oklch(95.79% 0.06 196.12deg);
86-
--color-tertiary-100: oklch(90.23% 0.08 204.23deg);
87-
--color-tertiary-200: oklch(84.73% 0.09 210.27deg);
88-
--color-tertiary-300: oklch(79.53% 0.1 214.93deg);
89-
--color-tertiary-400: oklch(74.25% 0.11 219.48deg);
90-
--color-tertiary-500: oklch(69.15% 0.12 224.2deg);
91-
--color-tertiary-600: oklch(62.59% 0.11 225.92deg);
85+
--color-tertiary-50: oklch(95.67% 0.06 196.16deg);
86+
--color-tertiary-100: oklch(90.09% 0.07 204.56deg);
87+
--color-tertiary-200: oklch(84.77% 0.09 210.41deg);
88+
--color-tertiary-300: oklch(79.34% 0.1 215.36deg);
89+
--color-tertiary-400: oklch(74.28% 0.11 219.59deg);
90+
--color-tertiary-500: oklch(69.17% 0.12 224.29deg);
91+
--color-tertiary-600: oklch(62.61% 0.11 226.01deg);
9292
--color-tertiary-700: oklch(55.61% 0.1 228.96deg);
93-
--color-tertiary-800: oklch(48.71% 0.09 232.4deg);
93+
--color-tertiary-800: oklch(48.72% 0.09 232.52deg);
9494
--color-tertiary-900: oklch(41.26% 0.09 237.16deg);
9595
--color-tertiary-950: oklch(33.71% 0.08 241.47deg);
9696
--color-tertiary-contrast-dark: var(--color-tertiary-950);
@@ -101,22 +101,22 @@
101101
--color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
102102
--color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
103103
--color-tertiary-contrast-500: var(--color-tertiary-contrast-dark);
104-
--color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
104+
--color-tertiary-contrast-600: var(--color-tertiary-contrast-dark);
105105
--color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
106106
--color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
107107
--color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
108108
--color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
109-
--color-success-50: oklch(94.27% 0.1 149.84deg);
110-
--color-success-100: oklch(91.45% 0.12 149.56deg);
111-
--color-success-200: oklch(88.45% 0.14 148.93deg);
112-
--color-success-300: oklch(85.79% 0.15 148.49deg);
113-
--color-success-400: oklch(82.97% 0.17 147.8deg);
114-
--color-success-500: oklch(80.53% 0.19 147.21deg);
115-
--color-success-600: oklch(73.14% 0.17 147.25deg);
116-
--color-success-700: oklch(65.56% 0.15 147.3deg);
117-
--color-success-800: oklch(57.52% 0.13 147.27deg);
118-
--color-success-900: oklch(49.44% 0.11 147.34deg);
119-
--color-success-950: oklch(41.01% 0.09 147.43deg);
109+
--color-success-50: oklch(94.14% 0.1 150.09deg);
110+
--color-success-100: oklch(91.32% 0.12 149.74deg);
111+
--color-success-200: oklch(88.34% 0.14 149.06deg);
112+
--color-success-300: oklch(85.73% 0.16 148.4deg);
113+
--color-success-400: oklch(82.92% 0.17 147.72deg);
114+
--color-success-500: oklch(80.48% 0.19 147.14deg);
115+
--color-success-600: oklch(70.96% 0.17 146.98deg);
116+
--color-success-700: oklch(60.85% 0.15 146.83deg);
117+
--color-success-800: oklch(50.63% 0.13 146.36deg);
118+
--color-success-900: oklch(39.55% 0.11 145.9deg);
119+
--color-success-950: oklch(27.93% 0.09 145.61deg);
120120
--color-success-contrast-dark: var(--color-success-950);
121121
--color-success-contrast-light: var(--color-success-50);
122122
--color-success-contrast-50: var(--color-success-contrast-dark);
@@ -130,17 +130,17 @@
130130
--color-success-contrast-800: var(--color-success-contrast-light);
131131
--color-success-contrast-900: var(--color-success-contrast-light);
132132
--color-success-contrast-950: var(--color-success-contrast-light);
133-
--color-warning-50: oklch(98.26% 0.1 108.02deg);
134-
--color-warning-100: oklch(95.84% 0.12 104.66deg);
135-
--color-warning-200: oklch(93.48% 0.13 102.21deg);
136-
--color-warning-300: oklch(91.49% 0.15 100.17deg);
137-
--color-warning-400: oklch(89.28% 0.16 98.19deg);
138-
--color-warning-500: oklch(87.14% 0.17 96.01deg);
139-
--color-warning-600: oklch(79.88% 0.16 96.31deg);
140-
--color-warning-700: oklch(72.35% 0.14 95.62deg);
141-
--color-warning-800: oklch(64.73% 0.13 95.92deg);
142-
--color-warning-900: oklch(56.77% 0.11 94.87deg);
143-
--color-warning-950: oklch(48.63% 0.1 95.22deg);
133+
--color-warning-50: oklch(98.19% 0.1 108.1deg);
134+
--color-warning-100: oklch(95.79% 0.12 104.81deg);
135+
--color-warning-200: oklch(93.46% 0.14 102.34deg);
136+
--color-warning-300: oklch(91.46% 0.15 100.34deg);
137+
--color-warning-400: oklch(89.26% 0.16 98.29deg);
138+
--color-warning-500: oklch(87.13% 0.17 96.05deg);
139+
--color-warning-600: oklch(77.06% 0.15 96.09deg);
140+
--color-warning-700: oklch(66.34% 0.13 96.05deg);
141+
--color-warning-800: oklch(55.5% 0.11 96.05deg);
142+
--color-warning-900: oklch(43.74% 0.09 95.97deg);
143+
--color-warning-950: oklch(31.46% 0.06 96.67deg);
144144
--color-warning-contrast-dark: var(--color-warning-950);
145145
--color-warning-contrast-light: var(--color-warning-50);
146146
--color-warning-contrast-50: var(--color-warning-contrast-dark);
@@ -156,39 +156,39 @@
156156
--color-warning-contrast-950: var(--color-warning-contrast-light);
157157
--color-error-50: oklch(89.99% 0.04 14.04deg);
158158
--color-error-100: oklch(83.49% 0.07 19.81deg);
159-
--color-error-200: oklch(77.4% 0.11 21.98deg);
160-
--color-error-300: oklch(72.13% 0.15 24.9deg);
161-
--color-error-400: oklch(67.39% 0.19 26.71deg);
162-
--color-error-500: oklch(63.72% 0.22 28.71deg);
163-
--color-error-600: oklch(59.28% 0.21 28.53deg);
164-
--color-error-700: oklch(54.92% 0.2 28.58deg);
165-
--color-error-800: oklch(50.51% 0.19 28.72deg);
166-
--color-error-900: oklch(46.22% 0.18 28.88deg);
167-
--color-error-950: oklch(41.86% 0.17 29.23deg);
159+
--color-error-200: oklch(77.5% 0.11 22.66deg);
160+
--color-error-300: oklch(71.84% 0.15 24.01deg);
161+
--color-error-400: oklch(67.29% 0.19 26.46deg);
162+
--color-error-500: oklch(63.71% 0.22 28.57deg);
163+
--color-error-600: oklch(56.71% 0.2 28.53deg);
164+
--color-error-700: oklch(49.68% 0.18 28.49deg);
165+
--color-error-800: oklch(42.15% 0.15 28.23deg);
166+
--color-error-900: oklch(34.59% 0.13 28.23deg);
167+
--color-error-950: oklch(26.16% 0.11 29.23deg);
168168
--color-error-contrast-dark: var(--color-error-950);
169169
--color-error-contrast-light: var(--color-error-50);
170170
--color-error-contrast-50: var(--color-error-contrast-dark);
171171
--color-error-contrast-100: var(--color-error-contrast-dark);
172172
--color-error-contrast-200: var(--color-error-contrast-dark);
173173
--color-error-contrast-300: var(--color-error-contrast-dark);
174174
--color-error-contrast-400: var(--color-error-contrast-dark);
175-
--color-error-contrast-500: var(--color-error-contrast-light);
175+
--color-error-contrast-500: var(--color-error-contrast-dark);
176176
--color-error-contrast-600: var(--color-error-contrast-light);
177177
--color-error-contrast-700: var(--color-error-contrast-light);
178178
--color-error-contrast-800: var(--color-error-contrast-light);
179179
--color-error-contrast-900: var(--color-error-contrast-light);
180180
--color-error-contrast-950: var(--color-error-contrast-light);
181181
--color-surface-50: oklch(100% 0 none);
182-
--color-surface-100: oklch(93.37% 0.01 308.7deg);
183-
--color-surface-200: oklch(86.74% 0.01 299.36deg);
184-
--color-surface-300: oklch(79.83% 0.01 304.18deg);
185-
--color-surface-400: oklch(72.93% 0.02 300.01deg);
186-
--color-surface-500: oklch(65.74% 0.03 301.77deg);
187-
--color-surface-600: oklch(57.91% 0.02 299.91deg);
188-
--color-surface-700: oklch(49.81% 0.02 297.75deg);
189-
--color-surface-800: oklch(41.22% 0.03 300.52deg);
190-
--color-surface-900: oklch(32.31% 0.03 298.09deg);
191-
--color-surface-950: oklch(22.74% 0.03 294.92deg);
182+
--color-surface-100: oklch(95.37% 0.01 308.7deg);
183+
--color-surface-200: oklch(86.54% 0.01 303.22deg);
184+
--color-surface-300: oklch(79.9% 0.02 301.23deg);
185+
--color-surface-400: oklch(72.79% 0.02 300.11deg);
186+
--color-surface-500: oklch(65.6% 0.03 301.53deg);
187+
--color-surface-600: oklch(57.52% 0.03 302.25deg);
188+
--color-surface-700: oklch(49.4% 0.03 300.57deg);
189+
--color-surface-800: oklch(40.57% 0.03 298.59deg);
190+
--color-surface-900: oklch(26.62% 0.03 296.2deg);
191+
--color-surface-950: oklch(21.68% 0.03 296.11deg);
192192
--color-surface-contrast-dark: var(--color-surface-950);
193193
--color-surface-contrast-light: var(--color-surface-50);
194194
--color-surface-contrast-50: var(--color-surface-contrast-dark);

0 commit comments

Comments
 (0)