Your first multi-app project in 15 minutes

Scaffold a Devora.js project, run it, build and serve the build, then add a second app that shares one function and one login with the first. Every command was run.

This is the shortest honest path through Devora.js's main idea: one project, several apps, one backend and one login. You scaffold a project, run it, build it and serve the build, then add a second app and call the same function from both. It is the repository's 15-minute tester guide (TESTING.md) written out, and every command and file below was run on a fresh project from the published packages (@devorajs/core and @devorajs/cli 0.3.5) on macOS with Node 24 and npm 11. I have not timed a first-time reader; the "15 minutes" is the guide's estimate.

1. Create the project

npm create devora@latest first-app
cd first-app

Choose the full-stack scope if it asks. You get three apps (marketing, dashboard, admin) and one shared backend in packages/backend, and dependencies are installed. New projects pin ^0.3.5 for both packages.

2. Run it

npm run dev

You should see the version in the header and each app's URL and routes. Dev ports start at 10000 and move up when one is taken, so read the URLs it prints:

devora.js 0.3.5  dev · 3 apps · ready in 118 ms

  marketing  auth: shared · marketing.example.com
    Local:    http://localhost:10000/
  dashboard  auth: shared · dashboard.example.com
    Local:    http://localhost:10001/
  admin      auth: shared · admin.example.com
    Local:    http://localhost:10002/

Open any of them and try /login: any username works, because the login is a demo with no password check. Stop the server with Ctrl+C.

3. Build, and serve the build

npm run build

The last line is built 3 apps in … with no errors. Now start the production server:

npm start

It refuses to start, with one line and exit status 1, until you say where login sessions are stored. That is deliberate, not a crash:

[devora] no session store configured: add sessions: { store: "memory" } under shared in
devora.config.ts (one long-running server only; for serverless or several instances use a path
to a module that default-exports a SessionStore backed by your database), and set
DEVORA_SESSION_SECRET (openssl rand -base64 32). See packages/backend/AUTH.md

Open devora.config.ts and add one line inside shared. "memory" keeps sessions in the server's memory, so it suits one long-running server on your machine, not serverless or several instances:

sessions: { store: "memory" },

Rebuild (the store choice is part of the build), set a secret, and start:

npm run build
export DEVORA_SESSION_SECRET="$(openssl rand -base64 32)"
npm start
[adapter-node] "marketing" → http://localhost:4173 ...
[adapter-node] "dashboard" → http://localhost:4174 ...
[adapter-node] "admin" → http://localhost:4175 ...

curl -i localhost:4173/ answers 200, and the response carries Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self' and X-Frame-Options: DENY: those security headers are on by default.

4. Add a second app and share a function

npx devora add reports --auth shared
npm install

That prints registered "reports" in devora.config.ts and created apps/reports. Now write a function once in the shared backend. packages/backend/greeting.ts:

import { serverFn } from "@devorajs/core";

export const greet = serverFn(async (input: { name: string }) => `Hello, ${input.name}!`);

Expose it by adding "./greeting": "./greeting.ts" to exports in packages/backend/package.json. Then create the same page in two apps, apps/marketing/routes/hello.tsx and apps/reports/routes/hello.tsx, changing the name in each (this is the version that passes npm run typecheck):

import type { RequestContext } from "@devorajs/core";
import { PageShell } from "@devorajs/core";
import { greet } from "@devorajs/backend/greeting";

export const renderMode = "ssr";

export async function loader(ctx: RequestContext) {
  return { message: await greet({ name: "marketing" }, ctx) };
}

export default function Hello({ data }: { data?: { message: string } }) {
  return (
    <PageShell appName="marketing">
      <h1>{data?.message}</h1>
    </PageShell>
  );
}

Run npm run dev again and open /hello on both apps. Each prints its own greeting, from the one function in packages/backend: Hello, marketing! on the marketing app and Hello, reports! on the reports app.

5. One login across the apps

The apps are auth: "shared", so one session works in all of them. With the production servers running (build again first, since you added an app), log in on the dashboard and use the cookie on the others:

J=/tmp/jar; T=$(curl -s -c $J localhost:4174/login | grep -o 'name="_csrf" value="[^"]*' | sed 's/.*value="//')
curl -s -b $J -c $J -o /dev/null -w "login %{http_code}\n" -d "_csrf=$T&username=ann" localhost:4174/login
for p in 4173 4174 4175; do curl -s -b $J -o /dev/null -w "account :$p %{http_code}\n" localhost:$p/account; done
curl -s -o /dev/null -w "no cookie %{http_code}\n" localhost:4173/account
login 303
account :4173 200
account :4174 200
account :4175 200
no cookie 401

The form's CSRF token is required: the same login without it is a 403. On localhost the three ports share one cookie; on real domains, sharing a login across subdomains needs shared.sessions.cookie.domain (see Security model).

Where to go next

Build a multi-app project goes further (the API app, the shared database client), Render modes explains the choice per route, and Deployment covers one Vercel or Netlify project per app.