Monorepo Setup - nself-org/nchat GitHub Wiki
Version: 0.9.2 Last Updated: February 12, 2026 Prerequisites: nSelf CLI v0.4.2+
This guide explains how to deploy multiple nSelf applications (e.g., Ι³Chat, Ι³TV, Ι³Family) that share a single backend, single authentication system, and single user baseβwhile maintaining per-app role-based access control (RBAC).
What You Get:
- π One authentication system - Users log in once across all apps (SSO)
- π₯ One user base - Single
auth.userstable shared by all apps - π Per-app roles - User can be admin in Ι³Chat, regular user in Ι³TV
- π Independent frontends - Each app has its own UI and features
- ποΈ Shared backend - One nSelf CLI instance serves all apps
- Architecture
- Prerequisites
- Directory Structure
- Step-by-Step Setup
- Per-App RBAC System
- User Role Management
- App Configuration
- Common Scenarios
- Troubleshooting
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Users' Browsers β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β Ι³Chat β β Ι³TV β β Ι³Family β β
β β :3000 β β :3001 β β :3002 β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββ¬ββββββ β
βββββββββΌββββββββββββββΌββββββββββββββΌββββββββββββββββββ
β β β
β βββββββββββΌββββββββββββββΌββββββ
βββββΊ Nginx Reverse Proxy β
β api.localhost β
βββββββββββ¬βββββββββββββββββββββ
β
βββββββββββββββΌββββββββββββββββββ
β β β
βββββββΌβββββββ βββββΌβββββββ βββββββββΌβββββββββ
β Hasura β β Auth β β Storage β
β GraphQL β β (Nhost) β β (MinIO) β
βββββββ¬βββββββ βββββ¬βββββββ βββββββββ¬βββββββββ
β β β
ββββββββββββββΌββββββββββββββββββ
β
βββββββββΌβββββββββ
β PostgreSQL β
β β
β Shared Tables:β
β - auth.users β
β - apps β
β - app_user_roles
β β
β App Tables: β
β - nchat_* β
β - ntv_* β
β - nfamily_* β
ββββββββββββββββββ
Shared Components:
- PostgreSQL database
- Hasura GraphQL engine
- Nhost Auth service
- MinIO storage
- User accounts (
auth.users)
Per-App Components:
- Frontend application (Next.js, React, etc.)
- App-specific tables (e.g.,
nchat_channels,ntv_videos) - App-specific roles (
app_user_rolestable) - App configuration (
NEXT_PUBLIC_APP_ID)
Cross-App Features:
- Single sign-on (SSO)
- Shared user profiles
- Cross-app notifications (optional)
-
nSelf CLI - Version 0.4.2 or later
npm install -g @nself/cli@latest
-
Multiple nSelf Apps - Clone the apps you want to run:
git clone https://github.com/nself/nself-chat.git git clone https://github.com/nself/nself-tv.git git clone https://github.com/nself/nself-family.git
-
System Requirements:
- Docker Desktop 20.10+
- Node.js 20.0+
- pnpm 9.0+
- 8GB RAM minimum (16GB recommended)
Create a monorepo directory structure:
mkdir nself-monorepo
cd nself-monorepoRecommended structure:
nself-monorepo/
βββ backend/ # Shared nSelf CLI backend
β βββ db/
β β βββ migrations/ # All migrations (ordered)
β βββ docker-compose.yml (generated by nself)
β βββ .env
βββ nchat/ # Ι³Chat application
β βββ frontend/
β βββ src/
β βββ package.json
β βββ .env.local
βββ ntv/ # Ι³TV application
β βββ frontend/
β βββ src/
β βββ package.json
β βββ .env.local
βββ nfamily/ # Ι³Family application
βββ frontend/
βββ src/
βββ package.json
βββ .env.local
mkdir -p nself-monorepo/{backend,nchat,ntv,nfamily}
cd nself-monorepocd backend
# Initialize nSelf CLI with demo configuration
nself init --demo
# Follow the prompts:
# β Project name: nself-monorepo
# β Enable services: PostgreSQL, Hasura, Auth, MinIO, Redis, MeiliSearch
# β Enable monitoring: Yes (Grafana + Prometheus + Loki)
# β Domain: api.localhostThis creates:
-
docker-compose.yml(all services) -
.env(backend configuration) - Database with core tables
cd ..
# Clone apps into monorepo
git clone https://github.com/nself/nself-chat.git nchat
git clone https://github.com/nself/nself-tv.git ntv
git clone https://github.com/nself/nself-family.git nfamilyCollect all database migrations into one place:
cd backend
mkdir -p db/migrations
# Copy migrations in order
cp ../nchat/backend/db/migrations/20260212_add_per_app_rbac.sql db/migrations/001_per_app_rbac.sql
cp ../nchat/backend/db/migrations/*.sql db/migrations/002_nchat_tables.sql
cp ../ntv/backend/db/migrations/*.sql db/migrations/003_ntv_tables.sql
cp ../nfamily/backend/db/migrations/*.sql db/migrations/004_nfamily_tables.sqlcd backend
nself start
# Wait for all services to start (2-3 minutes)
nself statusExpected output:
β PostgreSQL running on :5432
β Hasura running on :8080
β Auth running on :4000
β MinIO running on :9000
β Redis running on :6379
β MeiliSearch running on :7700
β Grafana running on :3000
cd backend
# Run migrations in order
for migration in db/migrations/*.sql; do
echo "Running $migration..."
nself exec postgres psql -U postgres -d nself -f "/migrations/$(basename $migration)"
doneVerify migrations:
nself exec postgres psql -U postgres -d nself -c "\dt public.app*"Expected tables:
public.appspublic.app_user_rolespublic.app_role_permissions
nself exec postgres psql -U postgres -d nselfThen run:
INSERT INTO public.apps (app_id, app_name, app_url, is_active) VALUES
('nchat', 'Ι³Chat', 'http://localhost:3000', true),
('ntv', 'Ι³TV', 'http://localhost:3001', true),
('nfamily', 'Ι³Family', 'http://localhost:3002', true);
-- Verify
SELECT * FROM public.apps;Ι³Chat (.env.local):
cd ../nchat/frontend
cp .env.example .env.localEdit .env.local:
# App Identity
NEXT_PUBLIC_APP_ID=nchat
NEXT_PUBLIC_APP_NAME=Ι³Chat
# Backend URLs (shared)
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth
NEXT_PUBLIC_STORAGE_URL=http://storage.localhost/v1/storage
# Environment
NEXT_PUBLIC_ENV=developmentΙ³TV (.env.local):
cd ../../ntv/frontend
cp .env.example .env.localEdit .env.local:
# App Identity
NEXT_PUBLIC_APP_ID=ntv
NEXT_PUBLIC_APP_NAME=Ι³TV
# Backend URLs (shared - SAME AS NCHAT)
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth
NEXT_PUBLIC_STORAGE_URL=http://storage.localhost/v1/storage
# Environment
NEXT_PUBLIC_ENV=developmentΙ³Family (.env.local):
cd ../../nfamily/frontend
cp .env.example .env.localEdit .env.local:
# App Identity
NEXT_PUBLIC_APP_ID=nfamily
NEXT_PUBLIC_APP_NAME=Ι³Family
# Backend URLs (shared - SAME AS NCHAT)
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth
NEXT_PUBLIC_STORAGE_URL=http://storage.localhost/v1/storage
# Environment
NEXT_PUBLIC_ENV=development# Ι³Chat
cd ../../nchat/frontend
pnpm install
# Ι³TV
cd ../../ntv/frontend
pnpm install
# Ι³Family
cd ../../nfamily/frontend
pnpm installOpen 3 terminals:
Terminal 1 - Ι³Chat:
cd nchat/frontend
pnpm dev
# Runs on http://localhost:3000Terminal 2 - Ι³TV:
cd ntv/frontend
pnpm dev --port 3001
# Runs on http://localhost:3001Terminal 3 - Ι³Family:
cd nfamily/frontend
pnpm dev --port 3002
# Runs on http://localhost:3002Visit any app (e.g., http://localhost:3000) and sign up:
- Email:
[email protected] - Password:
SecurePass123!
This creates a user in the shared auth.users table.
cd backend
nself exec postgres psql -U postgres -d nself-- Get the user ID
SELECT id, email FROM auth.users WHERE email = '[email protected]';
-- Assign roles (replace UUID with actual user ID)
INSERT INTO public.app_user_roles (app_id, user_id, role) VALUES
('nchat', 'user-uuid-here', 'owner'),
('ntv', 'user-uuid-here', 'admin'),
('nfamily', 'user-uuid-here', 'member');
-- Verify
SELECT * FROM public.app_user_roles;The per-app RBAC system allows users to have different roles in different applications while sharing authentication.
1. apps - Application Registry
CREATE TABLE public.apps (
id UUID PRIMARY KEY,
app_id TEXT UNIQUE NOT NULL, -- 'nchat', 'ntv', 'nfamily'
app_name TEXT NOT NULL, -- 'Ι³Chat', 'Ι³TV', 'Ι³Family'
app_url TEXT, -- 'https://chat.example.com'
is_active BOOLEAN DEFAULT true
);2. app_user_roles - Per-App User Roles
CREATE TABLE public.app_user_roles (
id UUID PRIMARY KEY,
app_id TEXT NOT NULL REFERENCES apps(app_id),
user_id UUID NOT NULL REFERENCES auth.users(id),
role TEXT NOT NULL, -- 'owner', 'admin', 'moderator', 'member', 'guest'
granted_by UUID REFERENCES auth.users(id),
granted_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ -- Optional expiration
);3. app_role_permissions - Per-Role Permissions
CREATE TABLE public.app_role_permissions (
id UUID PRIMARY KEY,
app_id TEXT NOT NULL REFERENCES apps(app_id),
role TEXT NOT NULL,
permission TEXT NOT NULL, -- 'channels.create', 'messages.delete'
resource TEXT -- Optional: specific resource ID
);| Role | Description | Typical Permissions |
|---|---|---|
| owner | Complete control | All permissions, billing, settings |
| admin | User/content management | Create/delete channels, manage users, moderation |
| moderator | Content moderation | Delete messages, warn/timeout users |
| member | Standard user | Send messages, join channels, upload files |
| guest | Limited access | View channels, read messages (no posting) |
Check permissions in React:
import { useAppPermissions } from '@/hooks/use-app-permissions'
function DeleteChannelButton({ channelId }: { channelId: string }) {
const { hasPermission, isAdmin } = useAppPermissions()
if (!hasPermission('channels.delete')) {
return null // Hide button if no permission
}
return (
<button onClick={() => deleteChannel(channelId)}>
Delete Channel
</button>
)
}Check roles:
const { hasRole, userRoles } = useAppPermissions()
if (hasRole('owner')) {
// Show owner-only features
}-- Grant admin role in Ι³Chat
INSERT INTO public.app_user_roles (app_id, user_id, role)
VALUES ('nchat', 'user-uuid', 'admin');
-- Grant member role in Ι³TV
INSERT INTO public.app_user_roles (app_id, user_id, role)
VALUES ('ntv', 'user-uuid', 'member');mutation GrantUserRole {
insert_app_user_roles_one(
object: {
app_id: "nchat"
user_id: "user-uuid"
role: "admin"
granted_by: "admin-user-uuid"
}
) {
id
role
}
}Create a trigger to assign default role to new users:
CREATE OR REPLACE FUNCTION assign_default_app_role()
RETURNS TRIGGER AS $$
BEGIN
-- Assign 'member' role in all active apps
INSERT INTO public.app_user_roles (app_id, user_id, role)
SELECT app_id, NEW.id, 'member'
FROM public.apps
WHERE is_active = true;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER on_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION assign_default_app_role();Each app must set:
# Required
NEXT_PUBLIC_APP_ID=nchat # Unique app identifier
NEXT_PUBLIC_APP_NAME=Ι³Chat # Display name
# Shared backend
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth
NEXT_PUBLIC_STORAGE_URL=http://storage.localhost/v1/storageGrant permissions in Hasura console (http://localhost:8080):
-
Track tables:
public.appspublic.app_user_rolespublic.app_role_permissions
-
Add permissions for
userrole:select: filter: user_id: { _eq: X-Hasura-User-Id }
-
Grant function permissions:
user_has_app_roleuser_has_app_permissionget_user_app_roles
Setup:
INSERT INTO public.app_user_roles (app_id, user_id, role) VALUES
('nchat', 'alice-uuid', 'admin'),
('ntv', 'alice-uuid', 'member');Result:
- Alice can create/delete channels in Ι³Chat
- Alice can only view/post content in Ι³TV
- Alice logs in once, seamless switch between apps
Grant temporary role:
INSERT INTO public.app_user_roles (app_id, user_id, role, expires_at)
VALUES ('nchat', 'bob-uuid', 'moderator', NOW() + INTERVAL '7 days');Result:
- Bob has moderator permissions for 7 days
- Role automatically expires
- No manual cleanup needed
Grant owner role everywhere:
INSERT INTO public.app_user_roles (app_id, user_id, role)
SELECT app_id, 'owner-uuid', 'owner'
FROM public.apps
WHERE is_active = true;Result:
- User has full control across all apps
- New apps automatically grant owner role (via trigger)
Symptom: User can log in but has no permissions.
Solution:
-- Check if user has roles
SELECT * FROM public.app_user_roles WHERE user_id = 'user-uuid';
-- If empty, assign default role
INSERT INTO public.app_user_roles (app_id, user_id, role)
VALUES ('nchat', 'user-uuid', 'member');Prevention: Implement the assign_default_app_role() trigger.
Symptom: GraphQL query returns "permission denied".
Solution:
-- Grant Hasura access
GRANT SELECT ON public.app_user_roles TO hasura;
GRANT SELECT ON public.app_role_permissions TO hasura;
GRANT EXECUTE ON FUNCTION public.user_has_app_role TO hasura;
GRANT EXECUTE ON FUNCTION public.user_has_app_permission TO hasura;Symptom: apps table not visible in Hasura.
Solution:
- Visit Hasura console: http://localhost:8080
- Data tab β Schema β public
- Track tables:
apps,app_user_roles,app_role_permissions - Reload metadata:
nself exec hasura hasura-cli metadata reload
Symptom: User must log in separately for each app.
Solution:
- Verify all apps use the same
NEXT_PUBLIC_AUTH_URL - Check that cookies have the same domain (e.g.,
.localhost) - In production, use a shared domain (e.g.,
*.example.com)
Symptom: Migrations fail due to table conflicts.
Solution:
- Prefix app-specific tables with app ID (e.g.,
nchat_channels,ntv_videos) - Use schema separation:
CREATE SCHEMA nchat; CREATE TABLE nchat.channels (...); CREATE SCHEMA ntv; CREATE TABLE ntv.videos (...);
- Security: Review Security Guide
- Production: Read Deployment Guide
- Scaling: Check Performance Guide
- Monitoring: Set up Observability
- ARCHITECTURE.md - Complete architecture documentation
- Per-App RBAC Types - TypeScript types
- useAppPermissions Hook - React hook
- GraphQL Queries - RBAC queries
Questions? Open an issue on GitHub.