DEV Community

Hassan Sayed
Hassan Sayed

Posted on

Why I Built a Framework Instead of Duct-Taping Three Next.js Apps Together

The problem I kept running into

Most real products aren't one app. There's a marketing site, a main app, and an admin
panel — usually sharing the same login and the same backend, but somehow always ending up
built and deployed as three separate things anyway.

Every time I hit this with Next.js, I had two bad options: duct-tape three separate Next.js
projects together with a shared package and hope nothing drifts, or fight a generic monorepo
tool that has no idea these three apps are supposed to share one login system.

So I built Devora.js. Multi-app is the actual
headline feature, not something bolted on afterward.

What it looks like in practice

One project, all your apps declared in a single config file:

export default defineProject({
  apps: [
    { name: "marketing", dir: "apps/marketing", domain: "example.com", auth: "none" },
    { name: "dashboard", dir: "apps/dashboard", domain: "app.example.com" },
    { name: "admin", dir: "apps/admin", domain: "admin.example.com", auth: "isolated" },
  ],
  shared: { core: "packages/core", auth: "shared" },
});
Enter fullscreen mode Exit fullscreen mode

That auth field per app is the part I care about most. It's a real three-way choice:

  • shared (the default) — marketing and dashboard log in once, share one session
  • isolated — admin gets its own completely separate session, its own cookie, its own secret. A dashboard session literally cannot authenticate against admin, by construction
  • none — no login on this app at all, so it needs zero session config. No secret to set, no warning at startup, nothing. And if a route accidentally tries to use sessions in a none app, the build fails right away with a clear message instead of quietly breaking in production

That none option didn't exist in my first pass, actually. Every app used to be forced to
configure a session secret whether it needed one or not — which meant a plain marketing page
was carrying the same setup burden as the login-gated admin panel. Fixing that felt like the
right kind of "explicit," where the config says exactly what's true instead of making you set
up something you'll never use.

Explicit over implicit, as a general rule

That's basically the whole design philosophy in one phrase. What I kept fighting with Next.js
was implicit caching and a private build format only one platform really understands. So here:

  • Rendering is chosen per route, out loud: ssr, ssg, csr, isr — no guessing which one you're getting
  • Deploys target Vercel's and Netlify's actual documented build specs, not a private format
  • ISR revalidation is a visible declaration — revalidate: { seconds: 3600 } — not a magic comment somewhere you have to remember exists

(Streaming render mode is deliberately not in v1 yet — more on why below.)

Security by default, not something you bolt on later

Every response ships with CSP, HSTS, and X-Frame-Options headers unless you explicitly turn
them off. Server functions are sandboxed — no dynamic eval or Function() anywhere in the
framework's own internals. Sessions are HMAC-signed and get rejected the moment someone tampers
with the cookie, which I actually tested with a hand-forged cookie rather than just assuming
the math works.

I'll be straight about this part: I built these defaults carefully, but nobody outside my own
head has actually tried to break them yet. If you do security work and have half an hour to
poke at something real, I would genuinely appreciate it. My contact info's on my GitHub
profile.

Islands, without leaving React

Partial hydration is a real, first-class thing you can reach for:

const Counter = island(() => import("./Counter"));
Enter fullscreen mode Exit fullscreen mode

The server renders the actual content immediately. Only that one component hydrates on the
client. Everything else on the page ships as plain static HTML. It's the real fix for React's
"the whole page has to hydrate" problem, without switching to Solid or Qwik to get there.

The honest limitation: it's built on a two-pass render, and that doesn't support streaming yet.
So right now renderMode: "streaming" just 404s instead of half-working — deferred to v2 on
purpose, rather than shipped in a broken half-state.

Where it actually stands right now

This is v1, built solo, but I've genuinely pushed it past "works on my machine." It's live on
both Vercel and Netlify for real — which, for what it's worth, surfaced bugs no amount of local
testing caught (a missing react/react-dom in the deployed function, a build silently
depending on NODE_ENV being set a certain way). It also runs in Docker, on a bare VPS behind
nginx or Caddy, has a GitHub Actions pipeline, and a Vitest suite covering sessions, CSRF,
every render mode, and islands.

npx create-devora@latest
Enter fullscreen mode Exit fullscreen mode

I'd really like to hear what people think of the architecture, and especially where the
security assumptions might be shakier than I think. What would you actually want from a
multi-app framework that this doesn't cover yet?

Top comments (0)