Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
14fd447
chore(license): relicense from AGPL-3.0 to MIT
urjitc Aug 2, 2026
42d826a
chore(deps): upgrade wrangler to 4.118.0
urjitc Aug 2, 2026
494ccc2
chore(deps): update in-range dependencies
urjitc Aug 2, 2026
29d68c4
fix(security): clear the remaining transitive advisories
urjitc Aug 2, 2026
8f19f07
fix(deploy): stop a bare deploy from overwriting production
urjitc Aug 3, 2026
f2bf21b
fix(telemetry): report extraction credits and stop inventing uploaders
urjitc Aug 3, 2026
880504b
fix(models): derive cost from real gateway prices
urjitc Aug 3, 2026
57f8a3d
feat(models): mark premium models in the picker
urjitc Aug 3, 2026
c2efe18
feat(billing): wire Autumn plans, usage, and env separation
urjitc Aug 3, 2026
85ee959
feat(settings): move settings into a dialog with plan and usage
urjitc Aug 3, 2026
953d1d8
docs(pricing): flag the landing price as a launch-time change
urjitc Aug 3, 2026
91f57bf
feat(billing): enforce AI message allowances
urjitc Aug 3, 2026
907e4eb
feat(billing): tell users what a message will cost them
urjitc Aug 3, 2026
27feb72
feat(billing): gate file uploads on the upload allowance
urjitc Aug 3, 2026
cb60e90
feat(pricing): publish Pro at $7.99 with real allowances
urjitc Aug 3, 2026
91c1273
fix(billing): keep the db layer out of the client bundle
urjitc Aug 3, 2026
4ee9365
fix(billing): close the gaps review found in allowance enforcement
urjitc Aug 3, 2026
0e24341
feat(billing): say which files still import at the upload cap
urjitc Aug 3, 2026
67f1e18
fix(billing): only treat an active subscription as Pro
urjitc Aug 3, 2026
b54c18e
test(billing): drop a fallback test that cannot fail
urjitc Aug 3, 2026
29eed7d
fix(billing): stop showing upgrade prompts to people who already pay
urjitc Aug 3, 2026
f8630ff
feat(telemetry): count the people a limit actually stops
urjitc Aug 3, 2026
fedc858
test(billing): catch a fallback that drifts to the priciest model
urjitc Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ BETTER_AUTH_URL="http://localhost:3000"
# AI_GATEWAY_API_KEY=""

# Autumn usage analytics. When unset, usage tracking is skipped.
# Sandbox key — staging and dev only. Production sets AUTUMN_PROD_SECRET_KEY
# instead, and never this, so a misconfigured deploy skips tracking rather than
# reporting into the wrong Autumn environment.
# AUTUMN_SECRET_KEY=""

# Web search and URL/PDF extraction (Firecrawl).
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dist-ssr
.tanstack
.content-collections/
.wrangler
*.cpuprofile
.tmp
tmp
.output
Expand All @@ -30,3 +31,6 @@ references
.dev.vars*
!.dev.vars.example
!.env.example

# Generated by `atmn pull`; declares @useautumn/sdk types we do not use.
@useautumn-sdk.d.ts
682 changes: 21 additions & 661 deletions LICENSE

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions autumn.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { feature, item, plan } from "atmn";

// Metered per message and per upload, one unit each — not by credit weight. The
// `cost` ladder in features/workspaces/ai/models.ts prices models against each
// other for routing, and deliberately does not reach allowances: students are
// not developers, and a balance they have to do arithmetic against is the thing
// that makes people ration instead of use the product. The two tiers are how
// model cost is reflected, which is as much arithmetic as anyone should face.

// Features
export const standardMessages = feature({
id: "standard_messages",
name: "Standard Messages",
type: "metered",
consumable: true,
});

export const premiumMessages = feature({
id: "premium_messages",
name: "Premium Messages",
type: "metered",
consumable: true,
});

// Extraction is billed per page by LlamaParse, but metered here per upload:
// nobody knows a PDF's page count before uploading, and a balance that drops by
// an unpredictable amount per upload is exactly the anxiety we're avoiding.
// Median upload is 5 pages, p90 is 31, so uploads track cost closely enough.
//
// Named "file uploads" rather than "documents" on purpose — Document is already
// a workspace item type, and two meanings of the word would confuse both users
// and us.
export const fileUploads = feature({
id: "file_uploads",
name: "File uploads",
type: "metered",
consumable: true,
});

// Plans
export const free = plan({
id: "free",
name: "Free",
autoEnable: true,
items: [
item({
featureId: standardMessages.id,
included: 500,
reset: {
interval: "month",
},
}),
// A taste of premium rather than a wall in front of it. 30 messages is
// roughly $0.90 of cost and it is the upgrade prompt.
item({
featureId: premiumMessages.id,
included: 30,
reset: {
interval: "month",
},
}),
// Matches NotebookLM's free 50 sources on purpose. Being stingier than the
// free competitor students already have open in a tab is a worse problem
// than eating the extraction cost.
item({
featureId: fileUploads.id,
included: 50,
reset: {
interval: "month",
},
}),
],
});

export const pro = plan({
id: "pro",
name: "Pro",
price: {
amount: 7.99,
interval: "month",
},
items: [
item({
featureId: standardMessages.id,
included: 3000,
reset: {
interval: "month",
},
}),
item({
featureId: premiumMessages.id,
included: 400,
reset: {
interval: "month",
},
}),
item({
featureId: fileUploads.id,
included: 500,
reset: {
interval: "month",
},
}),
],
});

// A premium message costs ~$0.031, so 100 of them cost ~$3.14 against $8.
export const premiumCreditsAddOn = plan({
id: "premium_credits_add_on",
name: "Premium Credits Add-on",
addOn: true,
items: [
item({
featureId: premiumMessages.id,
included: 0,
price: {
amount: 8,
billingUnits: 100,
billingMethod: "prepaid",
interval: "one_off",
},
}),
],
});
3 changes: 2 additions & 1 deletion docs/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ The dev server boots and lets you sign in with just the two variables below. Eve
| --- | --- | --- |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Google sign-in | Google sign-in fails; use **Continue as guest** (shown in local dev) |
| `AI_GATEWAY_API_KEY` | AI chat / title generation (Vercel AI Gateway) | AI calls error when invoked |
| `AUTUMN_SECRET_KEY` | Autumn usage analytics for completed AI chat messages | Usage tracking is skipped |
| `AUTUMN_SECRET_KEY` | Autumn usage analytics for completed AI chat messages, against the Autumn **sandbox**. Set in staging and dev only | Usage tracking is skipped |
| `AUTUMN_PROD_SECRET_KEY` | Same, against Autumn **production**. Set in production only, and takes precedence when both are present | Falls back to `AUTUMN_SECRET_KEY` |
| `FIRECRAWL_API_KEY` | Web search + URL/PDF extraction (Firecrawl) | Those calls error when invoked |
| `TCC_API_KEY` | Agent observability export (The Context Company) | AI chat still works; TCC export is skipped |
| `LLAMA_CLOUD_API_KEY` | Document extraction (LlamaParse) | Those calls error when invoked |
Expand Down
101 changes: 51 additions & 50 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"wrangler": "wrangler"
},
"dependencies": {
"@ai-sdk/react": "^4.0.40",
"@ai-sdk/react": "^4.0.51",
"@base-ui/react": "^1.6.0",
"@better-auth/oauth-provider": "1.6.25",
"@cloudflare/codemode": "^0.5.0",
"@cloudflare/codemode": "^0.5.1",
"@cloudflare/containers": "^0.3.7",
"@cloudflare/sandbox": "0.12.3",
"@cloudflare/shell": "^0.4.3",
Expand Down Expand Up @@ -69,8 +69,8 @@
"@embedpdf/plugin-viewport": "^2.14.4",
"@embedpdf/plugin-zoom": "^2.14.4",
"@fontsource-variable/geist": "5.3.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@posthog/ai": "^8.4.0",
"@modelcontextprotocol/sdk": "1.29.0",
"@posthog/ai": "^8.6.0",
"@posthog/mcp": "0.10.1",
"@posthog/react": "^1.10.3",
"@posthog/rollup-plugin": "^1.4.7",
Expand All @@ -83,45 +83,45 @@
"@tanstack/react-router-devtools": "1.167.0",
"@tanstack/react-router-ssr-query": "1.167.1",
"@tanstack/react-start": "1.168.32",
"@tiptap/core": "^3.29.1",
"@tiptap/extension-character-count": "^3.29.1",
"@tiptap/extension-code-block": "^3.29.1",
"@tiptap/extension-collaboration": "^3.29.1",
"@tiptap/extension-collaboration-caret": "^3.29.1",
"@tiptap/extension-highlight": "^3.29.1",
"@tiptap/extension-horizontal-rule": "^3.29.1",
"@tiptap/extension-link": "^3.29.1",
"@tiptap/extension-list": "^3.29.1",
"@tiptap/extension-mathematics": "^3.29.1",
"@tiptap/extension-placeholder": "^3.29.1",
"@tiptap/extension-table": "^3.29.1",
"@tiptap/extension-text-align": "^3.29.1",
"@tiptap/extension-underline": "^3.29.1",
"@tiptap/markdown": "^3.29.1",
"@tiptap/pm": "^3.29.1",
"@tiptap/react": "^3.29.1",
"@tiptap/starter-kit": "^3.29.1",
"@tiptap/core": "^3.29.2",
"@tiptap/extension-character-count": "^3.29.2",
"@tiptap/extension-code-block": "^3.29.2",
"@tiptap/extension-collaboration": "^3.29.2",
"@tiptap/extension-collaboration-caret": "^3.29.2",
"@tiptap/extension-highlight": "^3.29.2",
"@tiptap/extension-horizontal-rule": "^3.29.2",
"@tiptap/extension-link": "^3.29.2",
"@tiptap/extension-list": "^3.29.2",
"@tiptap/extension-mathematics": "^3.29.2",
"@tiptap/extension-placeholder": "^3.29.2",
"@tiptap/extension-table": "^3.29.2",
"@tiptap/extension-text-align": "^3.29.2",
"@tiptap/extension-underline": "^3.29.2",
"@tiptap/markdown": "^3.29.2",
"@tiptap/pm": "^3.29.2",
"@tiptap/react": "^3.29.2",
"@tiptap/starter-kit": "^3.29.2",
"agents": "^0.19.0",
"ai": "^7.0.37",
"autumn-js": "^1.2.44",
"ai": "^7.0.48",
"autumn-js": "^1.2.45",
"aws4fetch": "^1.0.20",
"better-auth": "^1.6.25",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-kit": "^0.31.9",
"drizzle-orm": "^0.45.1",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
"hast-util-to-string": "^3.0.1",
"katex": "^0.17.0",
"linkedom": "0.18.13",
"lucide-react": "^1.27.0",
"lucide-react": "^1.28.0",
"matter-js": "^0.20.0",
"mermaid": "11.16.0",
"motion": "^12.42.2",
"motion": "^12.43.0",
"nanoid": "^6.0.0",
"papaparse": "^5.5.4",
"partyserver": "^0.5.8",
"posthog-js": "^1.407.3",
"posthog-node": "^5.46.1",
"partyserver": "^0.5.9",
"posthog-js": "^1.409.5",
"posthog-node": "^5.47.3",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-resizable-panels": "^4.12.2",
Expand All @@ -131,26 +131,26 @@
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"shadcn": "^4.15.0",
"shiki": "^4.3.1",
"shadcn": "^4.16.1",
"shiki": "^4.4.1",
"sonner": "^2.0.7",
"streamdown": "^2.3.0",
"tailwind-merge": "^3.0.2",
"tw-animate-css": "^1.3.6",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"unified": "^11.0.5",
"unist-util-visit": "^5.1.0",
"y-indexeddb": "9.0.12",
"y-partyserver": "^2.2.0",
"y-protocols": "^1.0.7",
"yjs": "^13.6.31",
"zod": "^4.3.6",
"zod": "^4.4.3",
"zustand": "^5.0.14"
},
"devDependencies": {
"@babel/plugin-proposal-decorators": "^8.0.2",
"@cloudflare/vite-plugin": "^1.47.0",
"@cloudflare/vite-plugin": "^1.50.0",
"@cloudflare/vitest-pool-workers": "^0.18.8",
"@cloudflare/workers-types": "^5.20260727.1",
"@cloudflare/workers-types": "^5.20260801.1",
"@commitlint/cli": "^21.2.1",
"@commitlint/config-conventional": "^21.2.0",
"@rolldown/plugin-babel": "^0.2.3",
Expand All @@ -161,25 +161,26 @@
"@tanstack/react-devtools": "0.10.9",
"@tanstack/react-query-devtools": "5.101.4",
"@tanstack/router-plugin": "^1.168.23",
"@tiptap/y-tiptap": "^3.0.7",
"@tiptap/y-tiptap": "^3.0.8",
"@types/matter-js": "^0.20.2",
"@types/node": "^26.1.1",
"@types/node": "^26.1.2",
"@types/papaparse": "^5.5.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^6.0.4",
"@voidzero-dev/vite-plus-core": "^0.2.6",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"@voidzero-dev/vite-plus-core": "^0.2.7",
"atmn": "^1.1.16",
"babel-plugin-react-compiler": "^1.0.0",
"knip": "^6.17.1",
"knip": "^6.31.0",
"react-doctor": "0.6.0",
"tailwindcss": "^4.3.3",
"tsx": "^4.23.1",
"typescript": "^6.0.2",
"vite": "^8.0.16",
"typescript": "^6.0.3",
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
"vite-bundle-analyzer": "^1.3.9",
"vite-plus": "^0.2.6",
"vite-plus": "^0.2.7",
"vitest": "4.1.10",
"wrangler": "^4.114.0"
"wrangler": "^4.118.0"
},
"engines": {
"node": ">=24.11.0 <25"
Expand Down
Loading