Architecture - nself-org/nchat GitHub Wiki

Ι³Chat Architecture

Version: 1.0.9 Last Updated: 2026-04-18

Overview

Ι³Chat (nself-chat) is a FOSS team communication platform built as a reference implementation of what can be achieved with the nSelf CLI backend infrastructure. This document describes the architectural decisions, patterns, and setup instructions for both standalone and monorepo deployments.


Table of Contents

  1. Core Principles
  2. System Architecture
  3. Deployment Models
  4. Per-App RBAC/ACL
  5. Authentication Architecture
  6. Database Schema
  7. Frontend Architecture
  8. Backend Services
  9. Security Model
  10. Monorepo Setup Guide

Core Principles

1. Backend Exclusivity

Ι³Chat uses nSelf CLI exclusively for all backend operations:

  • βœ… Database: PostgreSQL via nSelf
  • βœ… GraphQL: Hasura via nSelf
  • βœ… Authentication: Nhost Auth via nSelf
  • βœ… Storage: MinIO/S3 via nSelf
  • βœ… Search: MeiliSearch via nSelf
  • βœ… Real-time: WebSocket subscriptions via Hasura

We do NOT use: Custom Express servers, Firebase, Supabase Auth, or any non-nSelf backend services.

2. Deployment Flexibility

The application supports two deployment models:

Standalone

user@host:~$ git clone nself-chat
user@host:~$ cd nself-chat
user@host:~$ nself start
user@host:~$ pnpm dev

One app, one backend, independent deployment.

Monorepo ("One of Many")

monorepo/
β”œβ”€β”€ backend/          # Shared nSelf backend
β”œβ”€β”€ nchat/            # This app
β”œβ”€β”€ ntv/              # Another app
└── nfamily/          # Another app

Multiple apps, one backend, shared authentication, per-app roles.

3. Per-App RBAC/ACL

In monorepo deployments, users authenticate once but can have different roles in different apps:

  • Admin in Ι³Chat, regular user in Ι³TV
  • Owner in Ι³Family, guest in Ι³Chat
  • Moderator in Ι³TV, member in Ι³Family

This is implemented via three PostgreSQL tables:

  • apps - Registry of all applications
  • app_user_roles - User roles per app
  • app_role_permissions - Permissions per role per app

System Architecture

High-Level Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Frontend Layer                           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”‚
β”‚  β”‚   Web    β”‚  β”‚  Mobile  β”‚  β”‚ Desktop  β”‚  β”‚  Admin   β”‚        β”‚
β”‚  β”‚ Next.js  β”‚  β”‚Capacitor β”‚  β”‚ Electron β”‚  β”‚  Panel   β”‚        β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜        β”‚
β”‚       β”‚             β”‚              β”‚             β”‚              β”‚
β”‚       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜              β”‚
β”‚                          β”‚                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                β”‚   Nginx Reverse     β”‚
                β”‚       Proxy         β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚                  β”‚                  β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    Hasura      β”‚ β”‚  Nhost Auth β”‚ β”‚  MinIO Storage  β”‚
β”‚  GraphQL API   β”‚ β”‚   Service   β”‚ β”‚   (S3-compat)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚                  β”‚                  β”‚
        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚
                   β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
                   β”‚   PostgreSQL   β”‚
                   β”‚   Database     β”‚
                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Technology Stack

Frontend

  • Framework: Next.js 15.1.6 (App Router)
  • UI: React 19 + Radix UI + Tailwind CSS
  • State: Zustand + Apollo Client
  • Real-time: GraphQL subscriptions + Socket.io
  • Editor: TipTap 2.11.2
  • Forms: React Hook Form + Zod

Backend (via nSelf CLI)

  • Database: PostgreSQL 15+ with 60+ extensions
  • GraphQL: Hasura Engine
  • Auth: Nhost Authentication
  • Storage: MinIO (S3-compatible)
  • Search: MeiliSearch
  • Cache: Redis
  • Monitoring: Grafana + Prometheus + Loki

Deployment Models

Standalone Deployment

Use Case: Independent installation, single team/organization.

Directory Structure:

nself-chat/
β”œβ”€β”€ backend/              # nSelf CLI project
β”‚   β”œβ”€β”€ migrations/       # DB migrations
β”‚   β”œβ”€β”€ docker-compose.yml
β”‚   └── .env
β”œβ”€β”€ frontend/             # Next.js app
β”‚   β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ platforms/
β”‚   └── package.json
└── README.md

Setup:

# 1. Initialize backend
cd backend
nself init
nself start

# 2. Start frontend
cd ../frontend
pnpm install
pnpm dev

Environment:

# frontend/.env.local
NEXT_PUBLIC_APP_ID=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

Monorepo Deployment ("One of Many")

Use Case: Multiple applications sharing authentication and users.

Directory Structure:

monorepo/
β”œβ”€β”€ backend/                # Shared nSelf backend
β”‚   β”œβ”€β”€ migrations/
β”‚   β”‚   β”œβ”€β”€ 001_create_users.sql
β”‚   β”‚   β”œβ”€β”€ 002_add_nchat_tables.sql
β”‚   β”‚   β”œβ”€β”€ 003_add_ntv_tables.sql
β”‚   β”‚   └── 004_add_per_app_rbac.sql
β”‚   └── docker-compose.yml
β”œβ”€β”€ nchat/                  # Ι³Chat app
β”‚   β”œβ”€β”€ frontend/
β”‚   └── package.json
β”œβ”€β”€ ntv/                    # Ι³TV app (example)
β”‚   β”œβ”€β”€ frontend/
β”‚   └── package.json
└── nfamily/                # Ι³Family app (example)
    β”œβ”€β”€ frontend/
    └── package.json

Setup:

# 1. Initialize shared backend
cd backend
nself init --demo
nself start

# 2. Run migrations for all apps
nself exec postgres psql -U postgres -d nself < migrations/001_create_users.sql
nself exec postgres psql -U postgres -d nself < migrations/002_add_nchat_tables.sql
nself exec postgres psql -U postgres -d nself < migrations/003_add_ntv_tables.sql
nself exec postgres psql -U postgres -d nself < migrations/004_add_per_app_rbac.sql

# 3. Start each app
cd ../nchat/frontend && pnpm dev
cd ../ntv/frontend && pnpm dev --port 3001
cd ../nfamily/frontend && pnpm dev --port 3002

App Environment Configuration:

# nchat/frontend/.env.local
NEXT_PUBLIC_APP_ID=nchat
NEXT_PUBLIC_APP_NAME=Ι³Chat
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth

# ntv/frontend/.env.local
NEXT_PUBLIC_APP_ID=ntv
NEXT_PUBLIC_APP_NAME=Ι³TV
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth

# nfamily/frontend/.env.local
NEXT_PUBLIC_APP_ID=nfamily
NEXT_PUBLIC_APP_NAME=Ι³Family
NEXT_PUBLIC_GRAPHQL_URL=http://api.localhost/v1/graphql
NEXT_PUBLIC_AUTH_URL=http://auth.localhost/v1/auth

Per-App RBAC/ACL

Overview

The per-app RBAC system allows users to have different roles in different applications while sharing a single user account and authentication session.

Database Schema

apps Table

CREATE TABLE public.apps (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id TEXT UNIQUE NOT NULL,        -- 'nchat', 'ntv', 'nfamily'
    app_name TEXT NOT NULL,             -- 'Ι³Chat', 'Ι³TV', 'Ι³Family'
    app_url TEXT,                       -- 'https://chat.nself.org'
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

app_user_roles Table

CREATE TABLE public.app_user_roles (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id TEXT NOT NULL REFERENCES apps(app_id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    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
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(app_id, user_id, role)
);

app_role_permissions Table

CREATE TABLE public.app_role_permissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id TEXT NOT NULL REFERENCES apps(app_id) ON DELETE CASCADE,
    role TEXT NOT NULL,
    permission TEXT NOT NULL,           -- 'channels.create', 'messages.delete'
    resource TEXT,                      -- Optional: specific resource ID
    created_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(app_id, role, permission, resource)
);

Role Types

Role Description Example Permissions
owner Complete control All permissions, billing, settings
admin User/channel management Manage users, create/delete channels, moderation
moderator Content moderation Delete messages, warn/timeout users, pin content
member Standard user Send messages, join channels, upload files
guest Limited access View channels, read messages (no posting)

Permission Format

Permissions use dot notation:

  • app.admin - Full app administration
  • users.manage - User management
  • users.ban - Ban users
  • channels.create - Create channels
  • channels.delete - Delete channels
  • messages.send - Send messages
  • messages.delete - Delete own messages
  • messages.delete.any - Delete any message
  • files.upload - Upload files
  • settings.manage - Manage app settings
  • billing.manage - Manage billing

Helper Functions

-- Check if user has role in app
SELECT user_has_app_role(
    'user-uuid',
    'nchat',
    'admin'
);

-- Check if user has permission in app
SELECT user_has_app_permission(
    'user-uuid',
    'nchat',
    'channels.delete',
    NULL  -- resource ID (optional)
);

-- Get all user roles for app
SELECT * FROM get_user_app_roles(
    'user-uuid',
    'nchat'
);

Frontend Integration

Hook Usage:

import { useAppPermissions } from '@/hooks/use-app-permissions'

function DeleteChannelButton({ channelId }: { channelId: string }) {
  const { hasPermission, isAdmin, loading } = useAppPermissions()

  if (loading) return <Skeleton />
  if (!hasPermission('channels.delete')) return null

  return (
    <button onClick={() => deleteChannel(channelId)}>
      Delete Channel
    </button>
  )
}

Context Access:

import { useAuth } from '@/contexts/auth-context'

function UserProfile() {
  const { user } = useAuth()

  return (
    <div>
      <h2>{user.displayName}</h2>
      <p>Roles in {process.env.NEXT_PUBLIC_APP_NAME}:</p>
      <ul>
        {user.appRoles?.map(role => (
          <li key={role}>{role}</li>
        ))}
      </ul>
    </div>
  )
}

GraphQL Queries

# Get user's roles in current app
query GetUserAppRoles($userId: uuid!, $appId: String!) {
  app_user_roles(
    where: {
      user_id: { _eq: $userId }
      app_id: { _eq: $appId }
      _or: [
        { expires_at: { _is_null: true } }
        { expires_at: { _gt: "now()" } }
      ]
    }
  ) {
    role
    granted_at
    expires_at
  }
}

# Grant a role to a user
mutation GrantUserRole(
  $appId: String!
  $userId: uuid!
  $role: String!
  $grantedBy: uuid
) {
  insert_app_user_roles_one(
    object: {
      app_id: $appId
      user_id: $userId
      role: $role
      granted_by: $grantedBy
    }
  ) {
    id
    role
  }
}

Authentication Architecture

Single Sign-On (SSO) in Monorepo

When multiple apps share a backend:

  1. User logs in to Ι³Chat β†’ Nhost creates session
  2. User visits Ι³TV β†’ Session is valid (same auth service)
  3. User visits Ι³Family β†’ Session is valid (same auth service)

All apps use the same:

  • auth.users table
  • Nhost JWT tokens
  • Session storage

App-Specific Context

While authentication is shared, app context is unique:

// User logs in once
user.id = "123"
user.email = "[email protected]"

// Context varies by app
// In Ι³Chat:
user.appRoles = ["admin"]
user.appContext.permissions = ["channels.delete", "users.ban"]

// In Ι³TV (same user):
user.appRoles = ["member"]
user.appContext.permissions = ["videos.view", "videos.upload"]

// In Ι³Family (same user):
user.appRoles = ["owner"]
user.appContext.permissions = ["*"]  // All permissions

Development Mode

For local development, Ι³Chat supports a test authentication mode:

# Enable test auth (8 predefined users)
NEXT_PUBLIC_USE_DEV_AUTH=true

Test users:

Password for all: password123

IMPORTANT: Never use NEXT_PUBLIC_USE_DEV_AUTH=true in production!


Database Schema

Core Tables

Table Purpose Rows (typical)
auth.users User accounts 1K-1M+
nchat_channels Chat channels 10-10K
nchat_messages Messages 100K-10M+
nchat_roles Role definitions 5-20
nchat_role_permissions RBAC permissions 50-200
apps App registry 1-50
app_user_roles Per-app roles 1K-1M+
app_role_permissions Per-app permissions 100-1K

Migration Strategy

Standalone:

cd backend
nself exec postgres psql -U postgres -d nself < migrations/init.sql

Monorepo:

# Order matters - run in sequence
cd backend
nself exec postgres psql -U postgres -d nself < migrations/001_create_users.sql
nself exec postgres psql -U postgres -d nself < migrations/002_add_per_app_rbac.sql
nself exec postgres psql -U postgres -d nself < migrations/003_add_nchat_tables.sql
nself exec postgres psql -U postgres -d nself < migrations/004_add_ntv_tables.sql

Row-Level Security (RLS)

All tables use RLS policies:

-- Example: Messages are viewable by channel members
CREATE POLICY "Users can view messages in their channels"
ON nchat_messages FOR SELECT
USING (
    channel_id IN (
        SELECT channel_id FROM nchat_channel_members
        WHERE user_id = auth.uid()
    )
);

-- Example: Apps are viewable by active app users
CREATE POLICY "Users can view their app roles"
ON app_user_roles FOR SELECT
USING (user_id = auth.uid());

Frontend Architecture

Directory Structure

frontend/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app/                    # Next.js App Router
β”‚   β”‚   β”œβ”€β”€ api/               # API routes
β”‚   β”‚   β”œβ”€β”€ auth/              # Auth pages
β”‚   β”‚   β”œβ”€β”€ chat/              # Main chat UI
β”‚   β”‚   β”œβ”€β”€ setup/             # Setup wizard
β”‚   β”‚   └── settings/          # User settings
β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ chat/              # Chat components
β”‚   β”‚   β”œβ”€β”€ ui/                # Radix UI wrappers
β”‚   β”‚   └── layout/            # Header, Sidebar
β”‚   β”œβ”€β”€ contexts/
β”‚   β”‚   β”œβ”€β”€ auth-context.tsx   # Auth state
β”‚   β”‚   └── app-config-context.tsx
β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ use-channels.ts
β”‚   β”‚   β”œβ”€β”€ use-messages.ts
β”‚   β”‚   └── use-app-permissions.ts  # RBAC hook
β”‚   β”œβ”€β”€ graphql/
β”‚   β”‚   β”œβ”€β”€ queries/
β”‚   β”‚   β”œβ”€β”€ mutations/
β”‚   β”‚   └── app-rbac.ts        # RBAC queries
β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”œβ”€β”€ app-rbac.ts        # RBAC types
β”‚   β”‚   └── index.ts
β”‚   └── lib/
β”‚       β”œβ”€β”€ apollo-client.ts
β”‚       └── utils.ts
β”œβ”€β”€ platforms/
β”‚   β”œβ”€β”€ mobile/                # Capacitor (iOS/Android)
β”‚   β”œβ”€β”€ desktop/               # Electron/Tauri
β”‚   └── README.md
β”œβ”€β”€ public/                    # Static assets
β”œβ”€β”€ tests/                     # Jest + Playwright
└── package.json

State Management

Global State (Zustand):

  • User preferences
  • Theme settings
  • UI state (modals, sidebars)

Server State (Apollo Client):

  • Channels
  • Messages
  • Users
  • App configuration

Real-time (GraphQL Subscriptions):

subscription OnNewMessage($channelId: uuid!) {
  nchat_messages(
    where: { channel_id: { _eq: $channelId } }
    order_by: { created_at: desc }
    limit: 1
  ) {
    id
    content
    user {
      id
      displayName
      avatarUrl
    }
    created_at
  }
}

Backend Services

nSelf CLI Services

Service Port Purpose
Hasura 8080 GraphQL API
Auth 4000 Authentication
PostgreSQL 5432 Database
MinIO 9000/9001 S3 storage
MeiliSearch 7700 Full-text search
Redis 6379 Cache/sessions
Grafana 3000 Monitoring
Prometheus 9090 Metrics
Loki 3100 Logs

Service Communication

Frontend ─┬─> Hasura (8080) ────> PostgreSQL (5432)
          β”œβ”€> Auth (4000) ───────> PostgreSQL (5432)
          β”œβ”€> MinIO (9000) ──────> S3 buckets
          └─> MeiliSearch (7700) ─> Search indices

All services run via Docker Compose generated by nSelf CLI:

cd backend
nself start        # Start all services
nself stop         # Stop all services
nself status       # Show status
nself logs hasura  # View logs
nself urls         # List all service URLs

Security Model

Authentication Flow

  1. User submits credentials β†’ Frontend sends to Nhost Auth
  2. Nhost validates β†’ Returns JWT access token + refresh token
  3. Frontend stores tokens β†’ Secure HTTP-only cookies (production)
  4. Subsequent requests β†’ Include JWT in Authorization header
  5. Hasura validates JWT β†’ Decodes user ID, enforces RLS

Authorization Layers

Layer 1: Hasura JWT Validation

  • All requests include JWT
  • Hasura verifies signature
  • Extracts x-hasura-user-id and x-hasura-role

Layer 2: Row-Level Security (RLS)

  • PostgreSQL policies enforce data access
  • Uses auth.uid() from JWT claims
  • Cannot be bypassed by GraphQL

Layer 3: Application Permissions (RBAC)

  • Frontend checks app_user_roles and app_role_permissions
  • UI hides/disables unauthorized actions
  • Backend validates via RLS policies

Rate Limiting

# Hasura console > API Limits
api_limits:
  depth_limit:
    global: 10
    per_role:
      user: 7
      anonymous: 5
  node_limit:
    global: 50
    per_role:
      user: 30
      anonymous: 10
  rate_limit:
    global:
      max_reqs_per_min: 120
      unique_params: IP
    per_role:
      user:
        max_reqs_per_min: 60
        unique_params: ["x-hasura-user-id"]

Monorepo Setup Guide

Step 1: Create Directory Structure

mkdir -p monorepo/{backend,nchat,ntv,nfamily}
cd monorepo

Step 2: Initialize Shared Backend

cd backend
nself init --demo
# Follow prompts:
# - Enable: PostgreSQL, Hasura, Auth, MinIO, MeiliSearch, Redis
# - Enable: Monitoring (Grafana + Prometheus + Loki)
# - Set admin password
# - Configure domain (e.g., api.localhost)

Step 3: Clone Apps

cd ..
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 nfamily

Step 4: Run Migrations

cd backend

# Core tables (users, auth)
nself exec postgres psql -U postgres -d nself < ../nchat/backend/db/migrations/20260212_add_per_app_rbac.sql

# App-specific tables
nself exec postgres psql -U postgres -d nself < ../nchat/backend/db/migrations/init_nchat.sql
nself exec postgres psql -U postgres -d nself < ../ntv/backend/db/migrations/init_ntv.sql
nself exec postgres psql -U postgres -d nself < ../nfamily/backend/db/migrations/init_nfamily.sql

Step 5: Configure Apps

# Ι³Chat
cd ../nchat/frontend
cp .env.example .env.local
# Edit NEXT_PUBLIC_APP_ID=nchat

# Ι³TV
cd ../../ntv/frontend
cp .env.example .env.local
# Edit NEXT_PUBLIC_APP_ID=ntv

# Ι³Family
cd ../../nfamily/frontend
cp .env.example .env.local
# Edit NEXT_PUBLIC_APP_ID=nfamily

Step 6: Start Services

# Terminal 1: Backend
cd backend
nself start

# Terminal 2: Ι³Chat
cd nchat/frontend
pnpm dev

# Terminal 3: Ι³TV
cd ntv/frontend
pnpm dev --port 3001

# Terminal 4: Ι³Family
cd nfamily/frontend
pnpm dev --port 3002

Step 7: Register Apps

Visit Hasura console (http://localhost:8080) and 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);

Step 8: Assign Initial Roles

-- Make first user an owner in all apps
INSERT INTO public.app_user_roles (app_id, user_id, role) VALUES
  ('nchat', 'user-uuid-here', 'owner'),
  ('ntv', 'user-uuid-here', 'owner'),
  ('nfamily', 'user-uuid-here', 'owner');

Troubleshooting

Issue: "User has no roles in this app"

Solution: Assign default role to new users via trigger:

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();

Issue: "Permission denied for app_user_roles"

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;

Issue: "Apps not showing in GraphQL"

Solution: Track tables in Hasura:

cd backend
nself exec hasura hasura-cli metadata reload

Or via console:

  1. Visit http://localhost:8080
  2. Data tab β†’ Schema β†’ public
  3. Track tables: apps, app_user_roles, app_role_permissions

Additional Resources


Changelog

v0.9.2 (February 12, 2026)

  • Added per-app RBAC/ACL system
  • Updated README with FOSS mission
  • Created ARCHITECTURE.md
  • Added monorepo setup documentation

v0.9.0 (February 6, 2026)

  • Restructured frontend to nself-family pattern
  • Fixed all TypeScript errors
  • Achieved 98%+ test pass rate
  • Production-ready build

License

MIT License - See LICENSE file for details.


Questions? Open an issue on GitHub.

⚠️ **GitHub.com Fallback** ⚠️