QUICK START B2B - nself-org/cli GitHub Wiki
Build a B2B platform with organization hierarchies, team management, custom branding per client, and usage tracking.
Time Estimate: 20-25 minutes Difficulty: Intermediate Prerequisites: Docker Desktop, basic understanding of multi-tenancy
Note: As of v0.9.6, commands have been consolidated. This guide uses the new v1.0 command structure:
nself orgโnself tenant orgnself whitelabelโnself dev whitelabelnself envโnself config env
A complete B2B platform with:
- Organization hierarchies (parent/child accounts)
- Team management with roles & permissions
- Custom branding per organization
- Usage tracking and billing per organization
- Admin dashboard for platform management
B2B Architecture:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Platform (Your Company) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
โ โ Client A โ โ Client B โ โ
โ โ (Acme) โ โ (TechCo) โ โ
โ โโโโโโโโโโโโโโค โโโโโโโโโโโโโโค โ
โ โ โข Team 1 โ โ โข Team 1 โ โ
โ โ โข Team 2 โ โ โข Team 2 โ โ
โ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
curl -sSL https://install.nself.org | bash
nself versionmkdir my-b2b-platform && cd my-b2b-platform
nself init --template b2bTemplate includes:
- Organization hierarchy schema
- Team & member management
- Role-based permissions
- Usage tracking tables
- Billing per organization
cat schema.dbmlKey tables:
Table organizations {
id uuid [pk]
parent_id uuid [ref: > organizations.id] // For hierarchies
name varchar
slug varchar [unique]
plan varchar
status organization_status
settings jsonb
created_at timestamp
}
Table organization_members {
id uuid [pk]
organization_id uuid [ref: > organizations.id]
user_id uuid [ref: > users.id]
role member_role
permissions jsonb
invited_by uuid
joined_at timestamp
}
Table teams {
id uuid [pk]
organization_id uuid [ref: > organizations.id]
name varchar
description text
settings jsonb
}
Table team_members {
id uuid [pk]
team_id uuid [ref: > teams.id]
user_id uuid [ref: > users.id]
role team_role
added_at timestamp
}
Table organization_usage {
id uuid [pk]
organization_id uuid [ref: > organizations.id]
metric_name varchar
quantity integer
timestamp timestamp
metadata jsonb
}
Table organization_settings {
organization_id uuid [pk, ref: - organizations.id]
branding jsonb
features jsonb
limits jsonb
notifications jsonb
}
nself build
nself startnself db schema apply schema.dbmlUsing Hasura console (https://api.local.nself.org):
mutation CreateParentOrg {
insert_organizations(objects: [
{
name: "Acme Corporation"
slug: "acme"
parent_id: null
plan: "enterprise"
status: "active"
settings: {
allowSubOrganizations: true
maxSubOrganizations: 10
maxMembers: 100
}
},
{
name: "TechCo Industries"
slug: "techco"
parent_id: null
plan: "business"
status: "active"
settings: {
allowSubOrganizations: true
maxSubOrganizations: 5
maxMembers: 50
}
}
]) {
returning {
id
name
slug
}
}
}mutation CreateChildOrgs {
insert_organizations(objects: [
{
name: "Acme Engineering"
slug: "acme-engineering"
parent_id: "acme-org-id-here"
plan: "team"
status: "active"
},
{
name: "Acme Marketing"
slug: "acme-marketing"
parent_id: "acme-org-id-here"
plan: "team"
status: "active"
}
]) {
returning {
id
name
parent_id
}
}
}mutation CreateTeams {
insert_teams(objects: [
{
organization_id: "acme-org-id"
name: "Backend Team"
description: "Backend development team"
settings: {
defaultRole: "developer"
allowExternalMembers: false
}
},
{
organization_id: "acme-org-id"
name: "Frontend Team"
description: "Frontend development team"
settings: {
defaultRole: "developer"
allowExternalMembers: false
}
}
]) {
returning {
id
name
organization_id
}
}
}mutation AddOrganizationMembers {
insert_organization_members(objects: [
{
organization_id: "acme-org-id"
user_id: "user-id-1"
role: "owner"
permissions: {
canInvite: true
canRemove: true
canManageTeams: true
canManageBilling: true
}
},
{
organization_id: "acme-org-id"
user_id: "user-id-2"
role: "admin"
permissions: {
canInvite: true
canRemove: false
canManageTeams: true
canManageBilling: false
}
},
{
organization_id: "acme-org-id"
user_id: "user-id-3"
role: "member"
permissions: {
canInvite: false
canRemove: false
canManageTeams: false
canManageBilling: false
}
}
]) {
returning {
id
role
user {
email
}
}
}
}mutation AddTeamMembers {
insert_team_members(objects: [
{
team_id: "backend-team-id"
user_id: "user-id-2"
role: "lead"
},
{
team_id: "backend-team-id"
user_id: "user-id-3"
role: "member"
}
]) {
returning {
id
team {
name
}
user {
email
}
}
}
}Access Hasura Console: https://api.local.nself.org
Table: organizations
Role: user - Can only see their own organizations
{
"organization_members": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}Role: owner - Full access to their organization + children
{
"_or": [
{
"id": {
"_eq": "X-Hasura-Organization-Id"
}
},
{
"parent_id": {
"_eq": "X-Hasura-Organization-Id"
}
}
]
}Table: organization_members
Role: owner - Can manage all members
{
"organization_id": {
"_eq": "X-Hasura-Organization-Id"
}
}Role: admin - Can view all, but limited updates
{
"organization_id": {
"_eq": "X-Hasura-Organization-Id"
},
"role": {
"_nin": ["owner"]
}
}Role: member - Read-only
{
"organization_id": {
"_eq": "X-Hasura-Organization-Id"
}
}Table: teams
All roles - Based on organization membership
{
"organization": {
"organization_members": {
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
}
}# As regular user
query GetMyOrganizations {
organizations {
id
name
role: organization_members(where: {user_id: {_eq: "X-Hasura-User-Id"}}) {
role
permissions
}
}
}
# As owner
query GetOrganizationHierarchy {
organizations(where: {parent_id: {_is_null: true}}) {
id
name
child_organizations {
id
name
}
members: organization_members {
user {
email
}
role
}
}
}nself dev whitelabel init# Brand for Acme
nself dev whitelabel branding create "Acme Corporation" \
--tenant acme \
--tagline "Building the future"
nself dev whitelabel branding set-colors \
--tenant acme \
--primary #0066cc \
--secondary #00cc66 \
--accent #ff6600
# Brand for TechCo
nself dev whitelabel branding create "TechCo Industries" \
--tenant techco \
--tagline "Technology that works"
nself dev whitelabel branding set-colors \
--tenant techco \
--primary #6600cc \
--secondary #cc0066 \
--accent #00ccffmutation UpdateOrganizationBranding {
update_organization_settings(
where: {organization_id: {_eq: "acme-org-id"}}
_set: {
branding: {
brandName: "Acme Corporation"
tagline: "Building the future"
colors: {
primary: "#0066cc"
secondary: "#00cc66"
accent: "#ff6600"
}
logo: {
main: "https://cdn.myapp.com/acme/logo-main.png"
icon: "https://cdn.myapp.com/acme/logo-icon.png"
}
domain: "app.acme.com"
}
}
) {
affected_rows
}
}# Add custom domain for Acme
nself dev whitelabel domain add app.acme.com --tenant acme
nself dev whitelabel domain verify app.acme.com
nself dev whitelabel domain ssl app.acme.com --auto-renew
# Add custom domain for TechCo
nself dev whitelabel domain add app.techco.com --tenant techco
nself dev whitelabel domain verify app.techco.com
nself dev whitelabel domain ssl app.techco.com --auto-renewExample: Track API calls
mutation TrackAPICall {
insert_organization_usage_one(object: {
organization_id: "acme-org-id"
metric_name: "api_calls"
quantity: 1
timestamp: "now()"
metadata: {
endpoint: "/api/v1/users"
method: "GET"
userId: "user-id-1"
}
}) {
id
}
}Example: Track storage
mutation TrackStorage {
insert_organization_usage_one(object: {
organization_id: "acme-org-id"
metric_name: "storage_bytes"
quantity: 1048576 # 1 MB
timestamp: "now()"
metadata: {
fileType: "image"
fileName: "logo.png"
}
}) {
id
}
}-- Usage by organization (last 30 days)
SELECT
o.name AS organization,
u.metric_name,
SUM(u.quantity) AS total_usage,
COUNT(*) AS usage_count
FROM organization_usage u
JOIN organizations o ON u.organization_id = o.id
WHERE u.timestamp > NOW() - INTERVAL '30 days'
GROUP BY o.name, u.metric_name
ORDER BY total_usage DESC;
-- Top consumers
SELECT
o.name AS organization,
SUM(u.quantity) AS total_usage
FROM organization_usage u
JOIN organizations o ON u.organization_id = o.id
WHERE u.metric_name = 'api_calls'
AND u.timestamp > NOW() - INTERVAL '7 days'
GROUP BY o.name
ORDER BY total_usage DESC
LIMIT 10;# Install Stripe plugin
nself plugin install stripeEdit .env:
STRIPE_API_KEY=sk_test_PLACEHOLDER_key_hereCreate subscriptions per organization:
mutation CreateSubscription {
insert_subscriptions_one(object: {
organization_id: "acme-org-id"
stripe_subscription_id: "sub_xxxxx"
plan: "enterprise"
status: "active"
current_period_start: "2026-01-01"
current_period_end: "2026-02-01"
price_amount: 9900 # $99.00
currency: "usd"
billing_interval: "month"
}) {
id
}
}Edit .env:
NSELF_ADMIN_ENABLED=truenself build && nself restartOpen: https://admin.local.nself.org
Admin features:
- View all organizations
- Manage members
- Track usage
- View billing
- Monitor health
-- Add admin role to user
UPDATE users
SET role = 'admin'
WHERE email = '[email protected]';nself config env create prodEdit .env.prod:
ENV=prod
PROJECT_NAME=my-b2b-platform
BASE_DOMAIN=myplatform.com
# Production database
POSTGRES_DB=b2b_prod
POSTGRES_PASSWORD=generate-secure-password
# Live Stripe
STRIPE_API_KEY=sk_live_your_key_here
# Security
HASURA_GRAPHQL_ADMIN_SECRET=generate-secure-secret
AUTH_JWT_SECRET=generate-jwt-secret
# Multi-tenancy
TENANT_ISOLATION_ENABLED=true
TENANT_COLUMN_NAME=organization_idnself deploy prodquery GetOrganizationTree {
organizations(where: {parent_id: {_is_null: true}}) {
id
name
plan
child_organizations {
id
name
plan
members_aggregate {
aggregate {
count
}
}
}
members_aggregate {
aggregate {
count
}
}
}
}query GetUserOrganizations($userId: uuid!) {
organization_members(where: {user_id: {_eq: $userId}}) {
role
permissions
organization {
id
name
slug
plan
teams {
id
name
team_members(where: {user_id: {_eq: $userId}}) {
role
}
}
}
}
}query GetOrganizationUsage($orgId: uuid!, $startDate: timestamp!, $endDate: timestamp!) {
organization_usage_aggregate(
where: {
organization_id: {_eq: $orgId}
timestamp: {_gte: $startDate, _lte: $endDate}
}
) {
aggregate {
sum {
quantity
}
count
}
nodes {
metric_name
quantity
timestamp
}
}
}// webhooks/organization-created.js
export async function onOrganizationCreated(organization) {
// Create default branding
await createBranding(organization.id, {
brandName: organization.name,
colors: getDefaultColors()
});
// Set up default teams
await createDefaultTeams(organization.id, [
{ name: "General", isDefault: true },
{ name: "Admins", isDefault: false }
]);
// Initialize usage tracking
await initializeUsageMetrics(organization.id);
// Send welcome email to owner
await sendEmail(organization.owner_email, "organization-welcome", {
organizationName: organization.name
});
}// webhooks/member-invited.js
export async function onMemberInvited(invitation) {
const { email, organization, role, invitedBy } = invitation;
// Send invitation email
await sendEmail(email, "team-invitation", {
organizationName: organization.name,
role: role,
inviterName: invitedBy.name,
inviteUrl: generateInviteUrl(invitation.token)
});
// Log activity
await logActivity(organization.id, {
type: "member_invited",
userId: invitedBy.id,
targetEmail: email,
role: role
});
}// Track usage via REST API
const trackUsage = async (organizationId, metric, quantity) => {
const response = await fetch('https://api.myplatform.com/v1/usage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
organization_id: organizationId,
metric_name: metric,
quantity: quantity,
timestamp: new Date().toISOString()
})
});
return response.json();
};
// Example usage
await trackUsage('acme-org-id', 'api_calls', 1);
await trackUsage('acme-org-id', 'storage_bytes', 1048576);subscription OrganizationActivity($orgId: uuid!) {
organization_usage(
where: {organization_id: {_eq: $orgId}}
order_by: {timestamp: desc}
limit: 10
) {
id
metric_name
quantity
timestamp
metadata
}
}# Check Hasura JWT configuration
nself config show hasura | grep JWT
# Verify user session has correct claims
# Session JWT should include:
# - X-Hasura-User-Id
# - X-Hasura-Organization-Id
# - X-Hasura-Role
# Test permissions
curl -X POST https://api.myplatform.com/v1/graphql \
-H "Authorization: Bearer ${JWT_TOKEN}" \
-d '{"query": "{ organizations { id name } }"}'# Check database connection
nself db query "SELECT COUNT(*) FROM organization_usage"
# Verify trigger exists
nself db query "
SELECT tgname
FROM pg_trigger
WHERE tgname LIKE '%usage%'
"
# Check recent usage
nself db query "
SELECT * FROM organization_usage
ORDER BY timestamp DESC
LIMIT 10
"# Verify DNS
nslookup app.acme.com
# Check nginx config
nself config show nginx | grep acme.com
# Test SSL
curl -I https://app.acme.com
# Check certificate
nself auth ssl check app.acme.com-- Add indexes for common queries
CREATE INDEX idx_org_members_org_id ON organization_members(organization_id);
CREATE INDEX idx_org_members_user_id ON organization_members(user_id);
CREATE INDEX idx_teams_org_id ON teams(organization_id);
CREATE INDEX idx_team_members_team_id ON team_members(team_id);
CREATE INDEX idx_usage_org_id_timestamp ON organization_usage(organization_id, timestamp DESC);
-- Partition usage table by month
CREATE TABLE organization_usage_2026_01 PARTITION OF organization_usage
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');Edit .env:
REDIS_ENABLED=true
HASURA_GRAPHQL_REDIS_URL=redis://redis:6379
HASURA_GRAPHQL_RATE_LIMIT={"unique_params":"IP","max_reqs_per_min":100}- Custom Domains Guide - Full domain setup
- Stripe Integration - Advanced billing
- White-Label System - Complete customization
- Database Workflow - Schema management
- Documentation: https://docs.nself.org
- GitHub: https://github.com/nself-org/cli
- Discord: https://discord.gg/nself
Your B2B platform is ready! Time to onboard your first client.