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.mdOpen 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/accountlogin 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.