Your first user is also your first tenant. Here's how we built account creation that scales.
When we designed Playwise HQ's competitive intelligence platform, we knew registration wasn't just about creating users. In multi-tenant SaaS, registration means creating organizational boundaries. Tenants. Permission structures. Data isolation layers.
We built account creation as the foundation of everything else.
The Account-First Decision
Most developers think registration means creating a user. We realized early that SaaS registration means creating a tenant first. Users belong to tenants. Data belongs to tenants. Permissions derive from tenants.
Here's the architectural choice that shaped everything:
// Account-first registration
const createAccount = async (registrationData) => {
return await withTransaction(async (session) => {
// Account creates the tenant boundary
const account = await Account.create([{
name: registrationData.accountName,
active: true,
accountType: 'organization'
}], { session });
// User belongs to the account
const user = await User.create([{
accountId: account[0]._id,
email: registrationData.email,
role: 'admin',
// ... other user fields
}], { session });
return { account: account[0], user: user[0] };
});
};
Account first. User second. This order matters for everything that follows.
Transaction-Based Consistency
We designed account creation as an atomic operation. Either the complete tenant setup succeeds, or nothing gets created. This prevents orphaned data and ensures consistent system state.
import { withTransaction } from '../../middleware/transaction.js';
export const createNewAccount = async (accountData) => {
// Validate availability outside transaction for performance
await checkEmailAvailability(accountData.email);
await checkAccountNameAvailability(accountData.accountName);
// Create both within transaction for atomicity
const result = await withTransaction(async (session) => {
const newAccount = await createNewAccountInDatabase(
accountData.accountName,
accountData.accountType,
session
);
const newUser = await createNewUserInDatabase({
accountId: newAccount._id,
email: accountData.email,
password: accountData.password,
role: 'admin'
}, session);
return {
accountId: newAccount._id,
userId: newUser._id,
accountName: newAccount.name,
email: newUser.email
};
});
return result;
};
The transaction boundary ensures data integrity. Performance optimizations happen outside the transaction. Critical operations happen inside.
Strategic Data Modeling
Every piece of data in our platform belongs to an account. This design choice enables perfect data isolation while maintaining query performance.
Here's our account model that anchors everything:
// Account.Model.js
const accountSchema = new mongoose.Schema({
name: {
type: String,
required: true,
unique: true // Global namespace for company names
},
active: {
type: Boolean,
default: true
},
accountType: {
type: String,
enum: ['individual', 'organization'],
default: 'organization'
},
subscription: {
tier: {
type: String,
enum: ['trial', 'starter', 'professional', 'enterprise'],
default: 'trial'
},
status: {
type: String,
enum: ['active', 'past_due', 'canceled'],
default: 'active'
},
limits: {
maxUsers: Number,
maxBattlecards: Number,
aiBattlecardGeneration: Boolean
}
}
}, {
timestamps: true
});
Everything flows from the account. Users reference it. Battlecards belong to it. Permissions derive from it.
Our user model enforces this relationship:
// User.Model.js
const userSchema = new mongoose.Schema({
accountId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Account',
required: true,
index: true // Optimized for tenant isolation queries
},
email: {
type: String,
required: true,
unique: true // Global email uniqueness
},
role: {
type: String,
enum: ['admin', 'manager', 'user'],
required: true
}
// ... other fields
});
// Compound index for efficient tenant queries
userSchema.index({ accountId: 1, email: 1 });
The accountId foreign key provides our multi-tenancy foundation. Every query benefits from this indexing strategy.
Admin Bootstrap Strategy
The first user who registers becomes the account admin automatically. This creates the perfect bootstrap scenario for B2B SaaS. The admin can then invite team members, assign roles, and manage account settings.
export const createNewUserInDatabase = async (userData, session) => {
const newUser = await User.create([{
accountId: userData.accountId,
email: userData.email,
password: userData.password,
firstName: userData.firstName || '',
lastName: userData.lastName || '',
role: userData.role || 'admin', // Bootstrap admin privileges
active: true,
emailVerificationStatus: 'pending'
}], session ? { session } : undefined);
return newUser[0];
};
This bootstrap admin gets full capabilities from day one:
- Invite team members
- Assign roles and permissions
- Manage subscription settings
- Transfer admin rights when needed
Scalable Role Architecture
Roles in our system are account-scoped and hierarchical. This design scales beautifully as organizations grow and need more granular permissions.
// Role middleware with account scoping
export const requireRole = (requiredRole) => {
return async (req, res, next) => {
const user = req.user; // From authentication middleware
// Check role within account context
const hasPermission = checkRolePermission(user.role, requiredRole);
if (!hasPermission) {
return res.status(403).json({
success: false,
error: 'Insufficient permissions for this account'
});
}
next();
};
};
const checkRolePermission = (userRole, requiredRole) => {
const roleHierarchy = {
'admin': ['admin', 'manager', 'user'],
'manager': ['manager', 'user'],
'user': ['user']
};
return roleHierarchy[userRole]?.includes(requiredRole) || false;
};
Admins inherit all capabilities. Managers get most features. Users get core functionality. All permissions respect account boundaries.
Subscription Integration from Day One
Account creation establishes the business relationship immediately. Every new account gets a structured trial experience with clear upgrade paths.
const createAccountWithSubscription = async (accountData) => {
const defaultSubscription = {
tier: 'trial',
status: 'active',
trialEndsAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000), // 14 days
limits: {
maxUsers: 5,
maxBattlecards: 10,
maxAIBattlecardsPerMonth: 3,
aiBattlecardGeneration: true
}
};
return await Account.create({
name: accountData.accountName,
subscription: defaultSubscription,
// ... other fields
});
};
Every account starts with generous trial limits. This subscription object drives feature access throughout the application:
// Feature gate middleware
export const requireFeature = (feature) => {
return async (req, res, next) => {
const account = await Account.findById(req.user.accountId);
const hasFeature = account.subscription.limits[feature];
if (!hasFeature) {
return res.status(402).json({
success: false,
error: 'This feature requires a paid subscription',
upgradeUrl: '/billing/upgrade'
});
}
next();
};
};
Features become subscription-aware from the foundation up.
Strategic Validation Design
Multi-tenant validation requires careful consideration of uniqueness constraints. We designed validation that prevents conflicts while enabling smooth user experiences.
// Global uniqueness validation
export const checkEmailAvailability = async (email) => {
const existingUser = await User.findOne({
email: email.toLowerCase()
});
if (existingUser) {
throw new ConflictError('Email is already in use', 'EMAIL_IN_USE');
}
};
// Account namespace validation
export const checkAccountNameAvailability = async (accountName) => {
const existingAccount = await Account.findOne({
name: accountName
});
if (existingAccount) {
throw new ConflictError(
'Account name is already in use',
'ACCOUNT_NAME_IN_USE'
);
}
};
Email addresses maintain global uniqueness. Account names create clear organizational namespaces. User roles operate within account boundaries.
Automatic Tenant Isolation
Every query in our system gets automatic tenant isolation. This architectural choice eliminates entire categories of security vulnerabilities while maintaining excellent performance.
// Automatic tenant context middleware
export const tenantIsolation = async (req, res, next) => {
// Inject tenant context into all requests
req.query.accountId = req.user.accountId;
// Provide explicit tenant context for manual queries
req.tenantContext = {
accountId: req.user.accountId,
userRole: req.user.role
};
next();
};
Every authenticated request carries tenant context. Every query respects account boundaries automatically.
Comprehensive Error Architecture
Our error handling provides clear feedback for multi-tenant scenarios while maintaining security boundaries.
// Structured error types
export class ConflictError extends Error {
constructor(message, code) {
super(message);
this.name = 'ConflictError';
this.statusCode = 409;
this.code = code;
}
}
// Account creation service with robust error handling
export const createNewAccount = async (accountData) => {
try {
// Validation and creation logic...
return result;
} catch (error) {
logger.error('Account creation process encountered an issue', {
error: { message: error.message, stack: error.stack }
});
// Preserve specific error types
if (error.statusCode) {
throw error;
}
// Wrap unexpected errors appropriately
throw new InternalServerError('Account creation unavailable', 'ACCOUNT_CREATION_ERROR');
}
};
Users get helpful error messages. Developers get detailed logging. Security boundaries stay intact.
The Architecture Win
This account-first approach created compound benefits throughout our platform:
Immediate Benefits:
Perfect data isolation between organizations
Clear permission boundaries from day one
Subscription features that work out of the box
Admin bootstrap that enables team growth
Long-term Advantages:
Query performance optimized for tenant isolation
Security architecture that scales automatically
Feature flagging tied to business models
Clean separation between user and organizational data
Developer Experience:
Consistent patterns across all features
Automatic tenant context in every request
Clear error messages for multi-tenant scenarios
Transaction patterns that prevent data inconsistencies
Your Multi-Tenant Foundation
Building account creation this way requires more upfront design work. The payoff is architecture that scales beautifully as your SaaS grows.
Start with accounts. Add users. Enforce boundaries. Everything else becomes easier.
The registration form users see is simple. The architecture underneath is sophisticated. That's exactly how SaaS should feel.
Your first user is your first tenant. Make sure your architecture treats them that way.
Great 👍