<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hassan Sayed</title>
    <description>The latest articles on DEV Community by Hassan Sayed (@hassanalsa3aka).</description>
    <link>https://dev.to/hassanalsa3aka</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4117604%2Fe0505925-9483-4505-ade0-c546aa23b98a.jpg</url>
      <title>DEV Community: Hassan Sayed</title>
      <link>https://dev.to/hassanalsa3aka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hassanalsa3aka"/>
    <language>en</language>
    <item>
      <title>Why I Built a Framework Instead of Duct-Taping Three Next.js Apps Together</title>
      <dc:creator>Hassan Sayed</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:12:32 +0000</pubDate>
      <link>https://dev.to/hassanalsa3aka/why-i-built-a-framework-instead-of-duct-taping-three-nextjs-apps-together-2561</link>
      <guid>https://dev.to/hassanalsa3aka/why-i-built-a-framework-instead-of-duct-taping-three-nextjs-apps-together-2561</guid>
      <description>&lt;h2&gt;
  
  
  The problem I kept running into
&lt;/h2&gt;

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

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

&lt;p&gt;So I built &lt;a href="https://github.com/hassanalsa3aka/devora.js" rel="noopener noreferrer"&gt;Devora.js&lt;/a&gt;. Multi-app is the actual&lt;br&gt;
headline feature, not something bolted on afterward.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;One project, all your apps declared in a single config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineProject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;marketing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apps/marketing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apps/dashboard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;apps/admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;isolated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;core&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packages/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shared&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;auth&lt;/code&gt; field per app is the part I care about most. It's a real three-way choice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shared&lt;/code&gt; (the default) — marketing and dashboard log in once, share one session&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;isolated&lt;/code&gt; — admin gets its own completely separate session, its own cookie, its own secret.
A dashboard session literally cannot authenticate against admin, by construction&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;none&lt;/code&gt; — 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 &lt;code&gt;none&lt;/code&gt;
app, the build fails right away with a clear message instead of quietly breaking in
production&lt;/li&gt;
&lt;/ul&gt;

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

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

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

&lt;p&gt;(Streaming render mode is deliberately not in v1 yet — more on why below.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Security by default, not something you bolt on later
&lt;/h2&gt;

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

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

&lt;p&gt;Partial hydration is a real, first-class thing you can reach for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;island&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./Counter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The honest limitation: it's built on a two-pass render, and that doesn't support streaming yet.&lt;br&gt;
So right now &lt;code&gt;renderMode: "streaming"&lt;/code&gt; just 404s instead of half-working — deferred to v2 on&lt;br&gt;
purpose, rather than shipped in a broken half-state.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where it actually stands right now
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-devora@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/hassanalsa3aka/devora.js" rel="noopener noreferrer"&gt;https://github.com/hassanalsa3aka/devora.js&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://devorajs-docs-docs.vercel.app" rel="noopener noreferrer"&gt;https://devorajs-docs-docs.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Packages: &lt;code&gt;@devorajs/core&lt;/code&gt;, &lt;code&gt;@devorajs/cli&lt;/code&gt;, &lt;code&gt;@devorajs/adapter-vercel&lt;/code&gt;,
&lt;code&gt;@devorajs/adapter-netlify&lt;/code&gt;, &lt;code&gt;create-devora&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
