Sovereign is a privacy-first, open-source collaboration and productivity suite that empowers individuals and organizations to take control of their digital lives. By providing a decentralized and federated platform, Sovereign will enables users to manage their data, communicate securely, and collaborate effectively while prioritizing privacy and self-determination.
The platform is still in its early stages of development.
While the plan has been mapped out, the documentation remains incomplete and is actively being developed.
We use Node.js and Express with the Handlebars template engine as the core stack for this application, with optional React SSR/JSX support. SQLite serves as the primary database during the MVP stage, with straightforward extensibility to PostgreSQL (or any other SQL database) through Prisma as an intermediate abstraction layer between the app code and the database.
Please refer Sovereign Wiki (WIP) for extended (evolving) documentation.
Sovereign is built as a modular platform. The core app provides the runtime (Express, Handlebars/React SSR, RBAC, settings, storage, CLI, and build system). Feature domains live in plugins that can be added, enabled/disabled, versioned, and shipped independently from the core.
Goals
- Minimize core surface area; keep features in plugins
- Enable safe iteration: plugin versioning + engine compatibility
- Support both server-rendered Handlebars and React SSR views
- Keep deploys simple: one app artifact with opt‑in plugins
High‑level runtime
- Bootstrap: core loads config, DB, logger, view engines, and scans
src/plugins/*for registered plugins. - Manifest phase: each plugin’s
plugin.jsonis validated (namespace, version, engine compatibility, entry points, declared routes/capabilities). - Wiring: core mounts plugin routes (web/api), registers handlers, exposes public assets, and loads views.
- RBAC merge: plugin‑declared capabilities are merged into the global graph (no runtime DB migration required for read‑time checks). (TBA)
- Lifecycle hooks (optional): install/enable/disable/upgrade hooks can prepare data, run migrations, or seed settings. (TBA)
Each plugin sits under src/plugins/<namespace> with a predictable layout. Example based on the uploaded screenshot:
src/
plugins/
blog/
handlers/ # business logic (service layer)
public/ # static assets served under /plugins/<ns>/...
routes/ # Express route modules (web + api)
views/ # Handlebars or React SSR views
index.mjs # plugin entry (exports attach(server) or factory)
plugin.json # manifest (see below)
papertrail/
During build, all files are copied to
dist/plugins/*with their original extensions, while code files are transpiled in place. This preserves the structure expected by the runtime and avoids*.json.json/*.html.htmlor.mjs -> .jsskew.
- Assets (
.html,.json,.css, images, etc.) are copied byte‑for‑byte. - Code files (
.ts,.tsx,.jsx,.js,.mjs,.cjs) are transpiled but keep their original extensions. - Core resolves
$imports tosrc/. - On startup, the server only mounts enabled plugins (see
isEnabled).
A plugin is defined by a plugin.json manifest with the index.mjs entry. The manifest declares compatibility, routes, and capabilities; the entry file wires handlers/routes when the plugin is enabled. Lifecycle hooks to be added in the future.
Below is the sample manifest used for the Blog plugin; field comments explain how the core interprets them.
Source: sample plugin.json shipped with the repo.
A conventional entry exposes a function the core calls with the app context. Minimal example:
// src/plugins/blog/index.mjs
/**
* Plugin: Route registry
* ----------------------
* Exposes the plugin's Express routers to the Sovereign extension host.
* - web: non-API routes (e.g., SSR pages)
* - api: REST or GraphQL endpoints for plugin data operations
*
* The extension host mounts these under a plugin-specific base path,
* e.g., `/api/<plugin-namespace>` for API and `/<plugin-namespace>` for web.
*
* This object should remain declarative and side-effect free.
*/
export const routes = { web: webRoutes, api: apiRoutes };
/**
* render
* ------
* Handles server-rendered index view for the plugin (if applicable).
*
* Typical Flow
* 1) Resolve and authorize request context
* 2) Prepare or fetch data relevant to the view
* 3) Render a Handlebars or React SSR template with that data
*
* Parameters
* - _: (reserved for dependency injection; receives context in future)
* - resolve(fn): wrapper that produces an Express route handler
*
* Returns
* - Express handler that renders a view or error template.
*
* Notes
* - This is optional; plugins without UI can omit it.
* - Avoid leaking secrets or raw config into templates.
*/
export async function render(_, resolve) {
return resolve(async (req, res) => {
// render logic goes here
});
}- Web routes mount under
/plugins/<namespace>by default; APIs under/api/plugins/<namespace>. - A plugin can customize its base mount path from the entry file. (TBA later)
- Views can be Handlebars or React SSR. Client hydration is supported via sibling
.client.jsxfiles when Vite is active in dev.
- Plugins declare capabilities in
plugin.json; these are merged into the global RBAC graph at boot. (TBA) - At request time, middleware exposes
req.can('user:plugin.blog.post.create')/res.locals.capabilitiesfor templates. - For idempotent imports or repeated enabling, capabilities are upserted.
- Core checks
manifest.sovereign.engineagainst the running engine version. Incompatible plugins are skipped with a warning. - Use semver for plugin
version; core can surface upgrade prompts when a newer compatible version is present.
sv plugins add <spec> # path | git URL | npm name
sv plugins list [--json] [--enabled|--disabled]
sv plugins enable <namespace>
sv plugins disable <namespace>
sv plugins remove <namespace>
sv plugins show <namespace> [--json]
sv plugins validate <path>
CLI tool is under development and not operational at the moment.
- Keep plugin code pure and self‑contained. Cross‑plugin calls should go via explicit APIs or events.
- Put shared utilities in
src/platformorsrc/services; do not reach into another plugin’s internals. - Keep manifests small: name/namespace/version, route toggles, capabilities, and minimal config. Heavy logic lives in code.
- macOS or Linux
- Node.js (v18+ recommended, v22.20.0+ for development)
- Yarn
- Configured the local workstation to push signed (via SSH/GPG) commits to GitHub.
-
Clone repo
git clone [email protected]:CommonsEngine/Sovereign.git cd Sovereign
-
Install
yarn install // or yarn
-
Configure environment
yarn init:prepare
init:preparescript will copy.env.example→.env- Update
.envwith required variables
-
Generate Prisma client and apply migrations
yarn prisma db push
-
Seed DB
yarn init:start
init:startscript will reset prisma, and the codebase if alreay configured, and run the seed script (yarn prisma:seed) after.- By default seed scripts will add App Settings, RBAC data.
-
Run app (example)
yarn dev // or yarn start
Use yarn dev to launch the development server with automatic file watching. For the production build, use yarn start.
- Updating Prisma schema and apply migrations
- Update
prisma/schema.prismafirst - Run
yarn prisma validateandyarn prisma formatto ensure the validity and format the schema changes - Run the migration command to log the change with
yarn prisma migrate dev --name <migration_name_in_snake_case>
- Update
The Sovereign Express/Handlebars stack also supports for React / JSX views (alonegside Handlebars) rendered via server-side rendering (SSR) with optional client-side hydration using Vite middleware.
This hybrid setup allows you to:
- Keep using Handlebars for static pages, layouts, and emails.
- Add React components or entire pages where interactivity or component reuse is needed.
- Render React SSR directly from Express routes using
res.renderJSX().
A custom Express helper/middleware, res.renderJSX(viewPath, props), is available to render React components server-side:
- It automatically resolves the module under
/src/views/${viewPath}.{jsx,tsx,ts,js}. - Uses React's SSR API to generate HTML and embed initial props.
- Automatically injects a matching client bundle (e.g.
.client.jsx) for hydration during development.
Example route (from src/index.mjs):
app.get(
"/example/react/*",
requireAuth,
exposeGlobals,
async (req, res, next) => {
try {
await res.renderJSX("example/react/index", {
path: req.params[0] || "",
});
} catch (err) {
next(err);
}
},
);The above renders the React component from src/views/example/react/index.jsx.
Example file: src/views/example/react/index.jsx
import React from "react";
import { Routes, Route, useParams, StaticRouter } from "react-router";
import { BrowserRouter, Link } from "react-router-dom";
function IndexPage() {
return (
<section>
<h2>Index Page (React App)</h2>
<p>
<Link to="/page/123">Go to Page 123</Link>
</p>
</section>
);
}
function PageById() {
const { id } = useParams();
return (
<section>
<h2>Page {id}</h2>
<p>Welcome!</p>
</section>
);
}
export default function ReactApp({ url }) {
const basename = "/example/react";
const isServer = typeof window === "undefined";
const content = (
<>
<header>
<h1>React App</h1>
<nav style={{ display: "flex", gap: 12 }}>
<Link to="/">Index Page</Link>
<Link to="/page/123">Page 123</Link>
</nav>
</header>
<Routes>
<Route path="/" element={<IndexPage />} />
<Route path="/page/:id" element={<PageById />} />
</Routes>
</>
);
return isServer ? (
<StaticRouter location={url} basename={basename}>
{content}
</StaticRouter>
) : (
<BrowserRouter basename={basename}>{content}</BrowserRouter>
);
}To hydrate the JSX page on the client, create a matching .client.jsx file in the same folder:
// src/views/example/react/index.client.jsx
import React from "react";
import { hydrateRoot } from "react-dom/client";
import ReactApp from "./index.jsx";
hydrateRoot(
document.getElementById("app"),
<ReactApp {...window.__SSR_PROPS__} />,
);When running in development (yarn dev), Vite automatically loads this client entry to hydrate the SSR HTML.
- JSX/TSX files are stored under
/src/views/, mirroring the Handlebars template structure. - In development, Vite runs in middleware mode (with HMR and JSX/TSX support).
- Production builds can extend Vite configuration to include client bundles for hydration.
- React Router v7+ is supported (
StaticRouterfromreact-router,BrowserRouterfromreact-router-dom). - Handlebars and React can be mixed — e.g., Handlebars layout wrapping a React-rendered
<div id="app">island.
Run the Node.js built-in test runner:
yarn testKeep tests running in watch mode during development:
yarn test:watchThe project uses a simple $ alias that points to the src/ directory. Instead of long relative paths like:
import logger from "../../services/logger.mjs";use:
import logger from "$/services/logger.mjs";The alias works for app code, tests, and development scripts (configured via a custom loader in scripts/alias-loader.mjs).
- AppSetting.value is a JSON column — it accepts objects, arrays, primitives and strings. Plain strings are stored as JSON strings.
- Feature flags: any env var prefixed with
FT_will be included infeature.flagsby the seed script (unlessALLOWED_FEATURESwhitelist is set). - User/email creation in seed and registration flows:
- User created first (without primaryEmailId)
- UserEmail created and linked with
userId - User updated with
primaryEmailIdreferencing created email
- Email delivery: configure
EMAIL_SMTP_URLorEMAIL_SMTP_HOST/EMAIL_SMTP_PORTwith credentials plusEMAIL_FROM_*env vars; toggle thefeature.email.delivery.bypassapp setting (orEMAIL_DELIVERY_BYPASSenv var) to disable outbound email while keeping logs for development. - Session RBAC snapshot:
- Sessions may store a server-side
rolesandcapabilitiesJSON to avoid repeated RBAC DB queries. - If roles/capabilities change, sessions must be invalidated or refreshed; consider versioning or updating session rows on changes. (To be implemented)
- Sessions may store a server-side
- "table ... does not exist": run migrations (
yarn prisma migrate deploy/yarn prisma migrate dev) andyarn prisma generate. - VersionRegistry increments: seed logic should update VersionRegistry once, not per-config. If values are unexpectedly high, ensure the upsert is executed only once.
We follow a Git Flow inspired branching strategy to keep development organized and production stable.
Branches
main→ production branch (always deployable).develop→ integration branch (latest development work).feat/→ short-lived branches for new features or fixes.release/→ optional branches to prepare a release.hotfix/→ urgent fixes branched from main.fix/→ bug fixes branched from develop.chore/→ maintenance tasks (docs, tooling, dependencies, CI), no product changes.improv/→ improvements
git switch -c feat/my-feature developWork, commit, and rebase with develop to stay updated.
- Use Squash & Merge to keep history clean.
- When develop is stable:
git checkout main
git merge --ff-only develop
git push origin mainAlternatively:
git fetch origin
git checkout develop
git pull # update local develop
git rebase origin/main # replay develop on top of main
# resolve any conflicts, then:
git push --force-with-lease
git checkout main
git merge --ff-only develop
git push- Tag the release:
git tag -a v1.2.0 -m "Release v1.2.0"
git push origin v1.2.0- Branch from
main, fix, then merge back into bothmainanddevelop.
Notes:
- Always branch out from
develop.- Do not rebase shared branches (
main,develop).- Rebase your local feature branches before opening a PR to keep history linear.
- Squash merges ensure each feature is a single, clean commit in history.
We encourage following Conventional Commits 1.0.0 for commit messages. Short guidelines:
- Format: type(scope?): subject
- type:
feat|fix|docs|style|refactor|perf|test|chore|build|ci|revert - scope: optional, single token describing area (e.g. auth, db, ui)
- subject: short, imperative, lowercase, no trailing period
- type:
- Optional body: blank line then detailed description (wrap ~72 chars)
- Footer: use for
BREAKING CHANGE:descriptions and issue references (e.g. "Refs: #123")
Examples:
- feat(auth): add invite token verification
- fix(register): validate invite token expiry
- docs(readme): clarify setup steps
- chore(deps): bump prisma to v6
- perf(cache): reduce redundant DB queries
- revert: Revert "feat(x): ..." (when reverting a previous commit)
Breaking change example (footer):
- feat(api): change user payload
- BREAKING CHANGE: "email" field moved from User -> UserEmail; update clients.
A multi-stage Dockerfile is provided to build and run Sovereign from a container. The image bundles the production build and Prisma client; SQLite data is stored under /app/data.
docker build -t sovereign:local .
mkdir -p ./data
# run with mounted volume for sqlite persistence
docker run --rm \
-p 5000:5000 \
-v $(pwd)/data:/app/data \
--env-file .env \
sovereign:localdocker build -t ghcr.io/<org>/<repo>:latest .
docker push ghcr.io/<org>/<repo>:latestEnsure you are logged in (docker login ghcr.io) with a PAT that has write:packages scope.
-
CI/CD push – On every merge to
main, build the image and push it toghcr.io/<org>/<repo>:<tag>(e.g.,latestplus a git SHA tag). GitHub Actions can handle this automatically. -
Server pull – On the target host:
docker login ghcr.io docker pull ghcr.io/<org>/<repo>:latest
-
Restart container with persistent volume – Reuse the same named volume (or host path) for
/app/dataso the SQLite file survives upgrades:docker stop sovereign || true docker rm sovereign || true docker run -d \ --name sovereign \ -p 5000:5000 \ -v sovereign-data:/app/data \ --env-file /opt/sovereign/.env \ ghcr.io/<org>/<repo>:latest
-
Verify – Check logs (
docker logs -f sovereign) and health endpoints. Because the data volume is external to the image, the SQLite database persists across deployments.
- Default
DATABASE_URLpoints to SQLite under/app/data; mount a persistent volume when running in production. - The entrypoint runs
prisma db pushon startup to sync the schema. Switch toprisma migrate deployonce a Postgres DB is introduced. - Container listens on port
5000; map it to any host port you prefer (e.g.,-p 5000:5000) and front with your preferred reverse proxy for TLS/HTTP termination.
If you prefer a bare-metal deployment without Docker, a sample ecosystem.config.cjs is included for PM2.
-
Install dependencies and build once:
yarn install --frozen-lockfile yarn build yarn prisma db push
Repeat the build step (
yarn build) after every application update sodist/stays current. -
Install PM2 globally (if not already):
npm install --global pm2
-
Start Sovereign with the provided config:
pm2 start ecosystem.config.cjs --env production pm2 status
-
Make the process restart on boot:
pm2 save pm2 startup
-
For updates:
git pull yarn install --frozen-lockfile yarn build yarn prisma db push pm2 reload sovereign
Environment variables come from your shell or an external manager (e.g., /etc/profile, systemd, direnv). The PM2 config sets PORT=3000 and NODE_ENV=production by default; override those with pm2 start ... --env or by editing ecosystem.config.cjs to suit your infrastructure.
Projects now support collaborative access with explicit membership records. Each project can include multiple owners, editors, and viewers:
- Owners can configure integrations, manage content, and invite or revoke other members.
- Editors can contribute to project content but cannot modify membership.
- Viewers have read-only access.
When registering a new account, any pending email-based project invites are automatically linked to the newly created user.
See Contributing to CommonsEngine/Sovereign
The community version licensed under AGPL-3.0.
{ "namespace": "blog", // unique short id used for routing + RBAC keys "name": "@sovereign/blog", // display + NPM-like naming convention "version": "1.0.0-alpha.7", // semver of the plugin itself "schemaVersion": 1, // manifest schema version (platform-side decoder) "preRelease": true, // whether the plugin is flagged experimental "isEnabled": true, // default enablement (can be toggled at runtime) "description": "Sovereign Blog", "main": "./index.mjs", // optional runtime entry for imperative wiring "author": "Sovereign Core Team", "license": "AGPL-3.0", "sovereign": { "engine": "0.5.0", // minimum/target core engine version compatibility "entryPoints": ["launcher"], // named entry points if the plugin exposes launchers. i.e: launcher | sidebar }, "routes": { "web": true, // mount web routes under /plugins/blog (or custom base) "api": true, // mount API routes under /api/plugins/blog }, "platformCapabilities": { "database": true, // requires DB access (Prisma) "gitManager": true, // requires Git manager integration "fs": true, // requires filesystem access to workspace }, "userCapabilities": [ // RBAC: capabilities added by this plugin { "key": "user:plugin.blog.feature", "description": "Enable Blog plugin.", "roles": ["platform:user"], }, { "key": "user:plugin.blog.create", "description": "Create blog project, and configure.", "roles": ["platform:user"], }, { "key": "user:plugin.blog.read", "description": "View own blog project.", "roles": [ "project:admin", "project:editor", "project:contributor", "project:viewer", "project:guest", ], }, // …additional granular post.* capabilities elided for brevity… ], "events": {}, // (reserved) event contracts the plugin can emit/consume "prisma": {}, // (reserved) plugin-owned Prisma schema modules "config": { "FT_PLUGIN_BLOG": true }, // default config/feature flags injected at init (note: might be removed later) }