Composable beats CMS for marketing sites under 200 pages
Most small marketing sites pay CMS costs for CMS problems they do not have. Components in a repo plus one thin data layer usually wins.

The default answer to "we need a website" has been "install a CMS" for twenty years, and for most marketing sites under 200 pages it is now the wrong default. A content management system earns its keep when many non-technical people publish frequently. When the honest answer is "the pages change a few times a year and one section changes weekly", you are paying the full cost of a publishing platform to serve what is mostly a brochure.
This is the architecture argument behind how we build, including how oxwynstudio.com itself is put together, so the receipts are inspectable.
What a CMS actually costs on a small site
The licence fee, if there is one, is the smallest line item. The real costs are structural.
Attack surface. A traditional CMS is a database-backed application with an admin login, a plugin ecosystem and a public rendering pipeline, all exposed on your domain. Every plugin is third-party code running with your site's privileges, written to varying standards, on its own update schedule. The overwhelming majority of compromised small-business sites are compromised through the CMS layer: an unpatched plugin, a stale theme, a brute-forced admin panel. None of that surface needs to exist for a site whose content changes quarterly.
The update treadmill. Core updates, plugin updates, PHP or runtime version bumps, compatibility breakage between them. Someone has to do this forever, and on small sites nobody is assigned to it, so it does not happen, which loops back to the previous paragraph. "Maintenance" retainers for CMS sites are substantially a fee for walking this treadmill on your behalf.
Render bloat. A general-purpose CMS assembles pages from a theme layer designed to accommodate any content anyone might ever enter. That flexibility ships as JavaScript, CSS and markup your specific pages do not need, and it shows up directly in Core Web Vitals. Hand-built pages have a performance ceiling a themed CMS practically never reaches; our own stated target is sub-300ms LCP (Largest Contentful Paint, the time until the main content is visible), which is a target you set when you control every byte, not when a page builder does.
Per-seat and per-feature licences. Headless CMS platforms moved the cost from servers to subscriptions: per-editor seats, per-locale charges, API rate tiers. Fine at scale. On a five-seat marketing site the invoice frequently exceeds the hosting bill of the entire rest of the stack by an order of magnitude.

What "composable" means here, concretely
Composable is a word vendors have stretched to mean "buy several SaaS products instead of one". We mean something narrower and cheaper.
Pages are components in a repository. The homepage, the service pages, the pricing tables on /website-as-a-service: all of it is code (React components in a Next.js App Router project, in our case), version-controlled in git, reviewed before deploy. A copy change is a commit. That sounds heavyweight until you compare it with reality on the other side: the CMS change was also supposed to go through someone, except there it goes through nobody, with no history and no rollback.
The gains compound. There is no admin panel for these pages because there is no runtime write path to them at all: they are compiled ahead of time and served static from a CDN. No plugin can take the pricing page down. A deploy either builds or it does not, and rollback is redeploying the previous commit.
A thin data layer only where content genuinely changes often. This is the part the static-site purists get wrong in the other direction. Some content really does change weekly, is written by more than one person, and should not require an engineer or a deploy. For us that is exactly one content type: these articles.
The middle path on oxwynstudio.com
The Insights section you are reading is served from a single Postgres table in Supabase (a hosted Postgres platform), written through a small admin panel with two roles, owner and writer. That table plus its security policies is the entire CMS. Here is its actual shape, from our migrations:
create table public.articles (
id uuid primary key default gen_random_uuid(),
slug text not null unique,
title text not null,
category text not null default 'Engineering',
excerpt text not null default '',
content_md text not null default '',
cover_image text,
status text not null default 'draft'
check (status in ('draft', 'in_review', 'published')),
author_id uuid references public.panel_users (id) on delete set null,
published_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table public.articles enable row level security;
-- Public readers see only published work; staff see everything.
create policy "published articles are public" on public.articles
for select using (status = 'published' or public.is_panel_user());
-- Writers edit their own unpublished work; owners edit anything.
create policy "edit articles" on public.articles
for update using (
public.is_owner() or (author_id = auth.uid() and status <> 'published')
) with check (
public.is_owner() or status in ('draft', 'in_review')
);
Read those two policies again, because they replace an entire CMS permissions module. RLS (row level security, Postgres rules that filter every query at the database itself) means the anonymous public role physically cannot select a draft, and a writer physically cannot push their own piece straight to published: the state machine (draft, in review, published) is enforced in the database, not in application code that a bug or a compromised admin route could bypass. The default posture is deny; every ability is an explicit grant you can read in one screen of SQL.
Compare the audit story. "Who can publish?" on a plugin-based CMS is an archaeology project across role plugins and capability filters. Here it is a grep.
The same logic covers everything else that changes on this site. Leads from the contact form land in a leads table only the owner role can read. The client register is another owner-only table. Each got a table and a policy instead of a plugin, and nothing else on the site touches a database at all.
The honest counter-cases
This architecture has a ceiling, and pretending otherwise is how agencies sell you the wrong thing.
Daily editorial teams. If five people publish every day, they need drafts with live preview, scheduling, an editorial calendar, asset management and inline review. That is a real product category, and a hand-rolled admin panel will grow into a bad clone of it one feature request at a time. Buy the CMS.
200-plus page content operations. Somewhere in the low hundreds of pages, content stops being pages and becomes a dataset: structured relationships, localisation, faceted search, bulk operations. You want structured content modelling, references between entries, and an API designed for it. That is what headless content platforms are actually for, and at that scale their pricing is justified.
Non-technical publishing at volume. If the people writing content will never touch git and there is no engineer in the loop, the repo-as-source-of-truth model fails socially, not technically. The bottleneck becomes a human with commit access, and content ships late or not at all. A CMS with a good editor is the correct tool.
The dishonest move is projecting these needs onto a 30-page site "for when we grow". Architecture for imagined scale is how a brochure ends up with a licence bill. You can migrate the day the editorial team exists; content in Markdown and Postgres exports cleanly, which is more than can be said for most CMS databases.

A decision checklist
Score your actual site, not the site you imagine in three years.
- How many pages exist today? Under 200: composable is viable. Well over: lean CMS or structured content platform.
- How often does each section really change? Audit the last twelve months of the current site. If most pages changed twice, they are code. Anything changing weekly earns a table.
- Who writes, and do they need to press publish themselves? Engineers in the loop: repo. Non-technical authors publishing independently: give that content type a data layer with roles, or a CMS if the types multiply.
- What does the permission model need to express? If it fits in a handful of roles and a state machine, RLS policies cover it in a page of SQL. If you need per-section permissions across dozens of editor groups, that is CMS territory.
- Who patches it in year three? A static site with one managed Postgres table has effectively nothing to patch. A CMS needs a named human on the treadmill. If no one will own that, the choice makes itself.
- What is the five-year cost, honestly? Seats, plugins, maintenance retainer, and the performance tax on every visit, against a repo, a free-tier database and CDN hosting. Run the numbers before the default runs them for you.
Most sites we are asked to quote land on the middle path: static pages in code, one thin authenticated data layer for the one or two content types that genuinely move, roles and RLS instead of a permissions module. It is cheaper to run, faster to load, and dramatically smaller as a target. You can see the result throughout our work, this site included.
Key takeaways
- A CMS's real costs on a small site are attack surface, a permanent update treadmill, render bloat and seat licences, not the headline fee.
- Composable, in the useful sense, means pages as version-controlled components plus a thin data layer only for content that genuinely changes often.
- On oxwynstudio.com that data layer is one Postgres table with row level security; two SQL policies replace an entire CMS permissions system.
- CMSs remain the right call for daily editorial teams, 200-plus page content operations and non-technical publishing at volume.
- Decide on the site you have, not the one you imagine: markdown and Postgres migrate out cleanly if you later outgrow the middle path.
Frequently asked
Is a static or composable site harder for my team to update?
For the pages that rarely change, updates go through the studio or an engineer as a small code change, which in practice is how CMS sites are updated anyway once nobody remembers the admin password. For content your team writes regularly, a thin admin panel over a database gives them a normal editing experience without a full CMS underneath.
Does no CMS mean no blog?
No. A blog is the classic case for the thin data layer: one table holding slug, title, markdown body and a status column, with database-level rules controlling who can publish. This article is served exactly that way. You get drafts, review states and public publishing without a plugin ecosystem attached.
At what point should we switch to a real CMS?
When publishing volume or team size makes the thin layer the bottleneck: several non-technical authors publishing most days, hundreds of structured content items, localisation, or editorial workflow needs like scheduling and preview across many content types. Content stored as markdown in Postgres exports cleanly, so the migration is a project, not a rewrite.
Is this approach more secure than a CMS, or just smaller?
Both, and the second causes the first. Static pages have no runtime write path to attack, and the only dynamic surface is a small authenticated panel over a database that denies everything by default. Fewer moving parts means fewer patches to miss, and row level security means even application bugs cannot leak unpublished or private rows.
Put this to work