Splitting the website from the dashboard

Why the front page and the console are now two products, what moved where, and what it costs to change the front page as a result.

Until this week scholasticus.app and the admin console were one application. The front page, the pricing page, the sign-in screen, the exam editor and the live proctoring monitor were all routes in the same TanStack Start app, built into one image, deployed as one unit.

That is a reasonable place to start and a bad place to stay.

What was actually wrong

Changing a sentence on the front page meant releasing the console. The console runs migrations on deploy, holds the RPC gateway every other product reaches the database through, and is the service whose death takes assessment with it. So a copy edit inherited the risk profile of a database migration, and the release checklist to match.

The consequence was predictable: the front page did not get edited. Not because anybody decided it was finished, but because the cost of touching it was set by something else entirely.

What moved

The apex — scholasticus.app — is now a separate product. It is an Astro site with its own repository, its own version, its own image and its own release. It holds the front page, the about page, the pricing page and this blog.

The console moved to dash.scholasticus.app. Everything behind sign-in is there, unchanged: workspaces, exams, grading, people, billing, the live monitor.

Tenant subdomains are unaffected. A candidate sitting an exam on your-institution.scholasticus.app reaches the same tenant application they always did.

What the site ships

Nothing, mostly. The only JavaScript on a marketing page is three small islands: the colour-mode switch, the mobile menu, and the billing-period toggle on the pricing page. There is no client-side router, no data fetching in the browser, and no framework runtime on the critical path.

Pages are rendered on demand rather than written to files, and that is a decision rather than a default. Prerendering would compile the console’s hostname into the HTML — and the console is on a different name in the test deployment than in production, so the image would be correct in one place only. Reading the origins at runtime keeps one artefact, and the edge caches the result, which is where a static file would have been served from anyway.

The design system is shared rather than reimplemented. The colours, the type scale, the button metrics and the plan cards all come from the same @scholasticus/ui package the console renders from, so the two halves of the product look like one product — and a token can only be changed in one place.

The one page that is not a file

/pricing is rendered per request, and it asks the console for the plan catalogue over the internal network.

This is the interesting constraint. The prices live in Polar. The console mirrors them and is the only thing that knows how to word them honestly — Polar sells five different ways, and only two of them have a cost that can be stated as one number. Copying that logic onto the site would have created a second place where a price is decided, and the failure mode of two places is a marketing page advertising something the checkout does not charge.

So the site asks, and the console answers with finished sentences. Repricing a plan in Polar changes the pricing page with no release of anything. If the console is unreachable the page still renders; it just says so instead of showing cards.

What it costs now

Editing the front page is a pull request against one repository, a build of one image, and a roll-forward of one service that holds no state and runs no migrations. The console is not involved and does not restart.

That is the whole point of the exercise.

More from the blog