I built osshp, an open-source, self-hostable platform for running your own portfolio/blog/photos site, and I run it as my real site, steili.com. If the idea of building your own version of "your platform" appeals to you (not a plugin, not a theme, a platform you actually own end to end), this is the path I'd recommend, based on what actually worked and what I'd have done sooner.
This isn't a tutorial with copy-pasteable code. It's the sequence of decisions that mattered, in the order I'd make them again.
Start with the ethos, because it determines every later decision
Before any code: write down, in one sentence, what "owning your platform" actually requires of you. Mine was this: self-host your data, no third-party lock-in, no trackers, and the operator (a single admin) has full control with no vendor able to unilaterally change the terms.
That sentence is not decoration. It's a filter you'll run every subsequent choice through. "Should I use a hosted auth provider?" No: that reintroduces a vendor between you and your login. "Should analytics be a third-party script?" No: that's a tracker, and it leaks your visitors to someone else's server. Write the ethos down first, because you will forget it under deadline pressure otherwise, and half of the "obviously wrong in hindsight" choices in any project come from skipping this step.
Pick a boring, proven stack, deliberately
Resist the urge to use this project as an excuse to try five new things at once. A self-hostable platform's job is to be dependable for someone who isn't you, running on hardware you'll never see. Every unfamiliar or bleeding-edge piece you add is a piece you'll be debugging blind, for a user you can't watch over their shoulder.
Concretely, that means: a mainstream server-rendered web framework, a relational database with decades of operational literature behind it, an object-storage interface that speaks a standard protocol (S3-compatible) rather than a specific vendor's API, and a reverse proxy that automates the part everyone gets wrong by hand (TLS certificate renewal). I chose Next.js + Bun, Postgres, an S3-compatible store accessed through a provider-agnostic client, and Caddy for automatic HTTPS. The specific choices matter less than the property they share: any of them can be replaced later without a rewrite, because none of them leaked vendor-specific assumptions into the rest of the codebase.
Actionable rule: if you can't name the swap-out path for a component before you've written a line of code against it, you've coupled to it too tightly. Decide the swap-out path first.
Make the security boundary a first-class constraint from day one, not a bolt-on
This is the one people skip and pay for later. Retrofitting security onto a codebase that wasn't built with it in mind is enormously more expensive than building it in from the start, because by the time you notice the gap, forty other things already assume it isn't there.
Concretely, decide these three things before you write your first content-rendering code, not after:
- Your Content Security Policy, and how strict it will be. A nonce-based CSP with no
'unsafe-inline'is a real constraint: it will reject libraries that write inline styles, it will reject inline<script>tags without a nonce, it will reject loading anything from a third-party origin you haven't explicitly allowed. Decide this early, because it will veto some of your dependency choices, and it's much cheaper to discover that in week two than to discover it after you've built three features on top of the library that fails the policy. - Your auth model, and where the recovery path's weak links are. Every recovery mechanism is itself an attack surface. Email recovery means your platform's security now depends on the security of an email provider you don't control. If you can build a strong primary method (I used passkeys) with recovery lanes that don't depend on a third party, do that instead.
- Your sanitization boundary for any user-authored content. If a human, even if that human is the only admin, writes Markdown, HTML, or anything that gets rendered back to a browser, decide exactly where the untrusted-to-trusted conversion happens and route everything through that one point. One well-tested sanitization boundary beats a dozen ad hoc escaping calls scattered through the codebase.
Building these in from day one doesn't cost you much time up front. Retrofitting them after forty routes assume a permissive CSP or an unsanitized render path costs you weeks.
Build modular before you need to: the cost is almost free if you do it now
Decide on a module or plugin contract before you build your second feature. Concretely: define an interface (what routes a feature registers, what admin-nav entries it contributes, what settings it exposes, what happens on enable/disable) and build your first feature against that interface too, even though it's currently the only feature that exists. It feels like overhead when there's only one module. It is not overhead: it's the thing that makes "I want feature A but not feature B" a supported configuration instead of a fork, and it's the reason your third feature takes a day instead of a week, because it's implementing an interface you've already exercised twice.
The same logic applies to your visual layer: define a rendering contract, then build exactly one theme against it. A second theme, if you ever want one, becomes a much smaller lift once the contract exists, and more importantly, your first theme staying inside the contract's boundaries proves the contract is real, not just aspirational.
Treat accessibility as a gate, not a pass you do at the end
Pick a concrete, testable standard (WCAG 2.1 AA is a reasonable default) and make it a requirement for every component you ship, checked at build time, not something you circle back for later. The reason this belongs early rather than late is the same reason security does: accessibility retrofitted onto components that weren't built with keyboard navigation and focus management in mind means re-architecting components, not patching them.
The practical shortcut: don't build your own low-level interactive primitives (dialogs, dropdowns, tabs, accordions) from scratch. Use a headless accessibility library that handles keyboard interaction, focus trapping, and ARIA wiring correctly, and build your own visual design on top of it. You get correct behavior you didn't have to get right yourself, and you keep full control over what it looks like. Don't adopt a full opinionated component library if you want your platform to have its own visual identity: the headless-primitive-plus-owned-styling split gives you both correctness and identity without a fight between them.
Self-host your data, actually, not "self-hosted" with an asterisk
If the point is owning your platform, your data layer needs to actually live on infrastructure you control: a real database you can dump and restore yourself, and object storage that speaks a standard protocol so you're never one API deprecation away from a rewrite. Build backup and restore as a real feature, encrypted at rest with an authenticated encryption scheme (not a hand-rolled cipher-plus-separate-HMAC construction; use a well-audited tool built for exactly this), and build lossless export/import of your actual content in a portable format, not a database dump only your own software can read. If you can't take everything your platform holds and walk away with it in a format a different tool could open, you haven't actually built something self-hostable: you've built something self-hosted.
The big one: dogfood it as your real site, early
This is the single highest-leverage thing on this list, and it's the one most tempting to skip because it feels risky to run unfinished software as your actual public-facing site.
Do it anyway, as soon as the core is functional enough to hold real content. The bugs that matter most don't show up in a test suite you wrote against your own mental model of how the software will be used; they show up when you're actually trying to publish something you care about, on a deadline, with a real photo from a real phone, and the thing you didn't think to test breaks in your face. Large file uploads that fail silently. A photo format your test fixtures never included. Metadata that isn't being read correctly on an image that actually has that metadata. A hosting mode you need because your actual internet connection has a dynamic IP.
None of that shows up on a feature checklist. It shows up when the software has to survive contact with your actual life. Every one of those bugs is cheaper to find yourself, on your own site, before anyone else is depending on the software, than to have a stranger find it for you after you've called it done.
A suggested order, if you want one
- Write the one-sentence ethos. Keep it visible.
- Pick your boring stack; for each piece, know the swap-out path before you write against it.
- Decide your CSP strictness, your auth/recovery model, and your sanitization boundary. Build nothing that violates them, from the first line of code.
- Define your module contract and your theme/rendering contract. Build your first feature and your first theme as if they were going to be swapped out or joined by a second one.
- Build the smallest complete vertical slice (one content type, rendered, sanitized, styled, accessible) before adding a second one.
- Wire up backup/restore and lossless export/import before you call anything a release. If you can't walk away with your data today, it isn't done.
- Ship the smallest version that's honestly usable, then move your real content onto it and use it as your real site.
- Fix what dogfooding actually surfaces, not what you imagine it might surface, before adding the next feature.
None of this is exotic advice. It's just the order that keeps the expensive mistakes (retrofitted security, retrofitted accessibility, retrofitted modularity, retrofitted data-portability) from ever getting the chance to happen, because you paid the small early cost instead of the large late one.
If you want a working reference for what this looks like end to end, osshp is open-source and AGPL-3.0 licensed; feel free to read the code, take the parts that are useful, and build your own.
