Stop Thinking “User Signup”. Start Thinking “New Tenant”

In multi-tenant SaaS, data isn’t global, it’s tenant-scoped. Data belongs to accounts. Permissions derive from accounts. We made a conscious decision early: treat account creation as the root of the architecture.


This content originally appeared on HackerNoon and was authored by Paul Towers

In any multi-tenant SaaS product, the moment a new user signs up is more than just adding a row in a “users” table. That user represents a new team, a new data boundary, and a new permission structure.

We made a conscious decision early: treat account creation as the root of the architecture.

Instead of bolting on organizations after user creation or storing org data as just another field on the user we made the account the primary object. Users belong to accounts. Data belongs to accounts. Permissions derive from accounts.

That framing influenced every piece of our backend from schema design to billing logic to RBAC.

Here’s how we built it.

Rethinking Registration: Account Before User

Most SaaS onboarding logic looks like this:

POST /signup → Create user → Later: create org / assign team

But in multi-tenant SaaS, that order leads to subtle problems…data without a home, permissions in limbo, and fragile assumptions throughout your app.

So we started with:

POST /signup → Create account → Create user inside account → Done

This change might seem small, but it shaped everything about our architecture going forward.

The code looks something like this:

export async function registerAccount(payload: RegistrationDTO) {
  return withTransaction(async (session) => {
    const account = await AccountModel.create([{
      name: payload.company,
      active: true
    }], { session });

    const user = await UserModel.create([{
      accountId: account[0]._id,
      email: payload.email,
      role: 'admin'
    }], { session });

    return { account: account[0], user: user[0] };
  });
}

By tying the user directly to the account from day one and wrapping it in a transaction we avoided a long list of edge cases that otherwise creep in fast.

Why It Matters: Data Belongs to Tenants

Here’s what we learned the hard way: data isn’t global, it's tenant-scoped.

Every query, permission check, feature gate, and UI control in your app will behave differently depending on who the user is and what account they belong to.

So we made one rule:

Everything important carries an accountId.

That includes:

  • Users
  • Battlecards
  • Permissions
  • Subscriptions
  • Audit logs
  • Feature toggles

Keep Transactions Tight

When building the account registration flow, one of our key principles was separating validation from persistence and being intentional about where we used database transactions.

Here’s what that looks like in practice:

\

await ensureEmailFree(payload.email);
await ensureCompanyFree(payload.company);

return withTransaction(async (session) => {
  // Only the critical writes happen here
  const account = await Account.create([{ name: payload.company }], { session });
  const user = await User.create([{ email: payload.email, accountId: account[0]._id }], { session });

  return { account: account[0], user: user[0] };
});

Why? So the transaction stays small and fast, easier to scale, fewer lock issues, and easier to recover from failures.

Bootstrap Admins, Automatically

The first user inside any account becomes its admin. No special logic, no manual flag-setting. It’s built right into the flow.

That first user can:

  • Invite their team
  • Set roles and permissions
  • Configure billing
  • Transfer admin rights if they leave

This gave us a clean, hands-off onboarding path that scaled well even in the early days.

Account-Scoped Roles that Grow With You

We didn’t want to overbuild a role system but we needed enough structure to cover 90% of real-world SaaS use cases.

\

const ROLE_HIERARCHY = {
  admin: ['admin', 'manager', 'user'],
  manager: ['manager', 'user'],
  user: ['user']
};

function hasPermission(userRole, requiredRole) {
  return ROLE_HIERARCHY[userRole]?.includes(requiredRole) || false;
}

We scoped roles to accounts and used simple inheritance. It’s not fancy, but it works and it’s dead easy to refactor later into a more granular policy model if needed.

Subscriptions from Day One

We gave every account a subscription object at creation, even during trial. That meant feature flags, usage limits, and upgrade prompts were tied directly to the account from the beginning.

const defaultSubscription = {
  tier: 'trial',
  status: 'active',
  limits: {
    maxUsers: 5,
    maxBattlecards: 10,
    aiBattlecardGeneration: true
  }
};

We use this in middleware to gate features:

if (!account.subscription.limits.aiBattlecardGeneration) {
  return res.status(402).json({ error: 'Upgrade required' });
}

Namespaces, Uniqueness, and Isolation

Some things are globally unique. Some are tenant-local. We drew the line like this:

| Field | Scope | Why | |----|----|----| | email | Global | One user = one email | | account.name | Global | Used in URLs and billing | | Other data | Tenant | Scoped by accountId |

What We Got Right (And What You Can Steal)

This small shift, making account creation the center of onboarding, paid off immediately and continues to save us time today.

Short-Term Wins:

  • Clean data isolation
  • Role-based access from day one
  • Feature flag logic that actually works
  • Fully functional trial flow

Long-Term Payoffs:

  • Queries are easy to scope and optimize
  • Billing and permissions are decoupled
  • No weird migration needed when you “add teams later”
  • Safer defaults everywhere

Final Thoughts: Start With the Right Mental Model

The registration page might look simple, but it kicks off the most important structure in your SaaS: the tenant.

So build like it matters.

Start with the account. Attach everything else to it. Enforce boundaries early. Everything else, roles, permissions, billing, falls into place more naturally.

Instead of thinking “user-first” and instead thinking “account-first” gave us an architecture we could trust… and build on.


This content originally appeared on HackerNoon and was authored by Paul Towers


Print Share Comment Cite Upload Translate Updates
APA

Paul Towers | Sciencx (2025-07-09T07:32:18+00:00) Stop Thinking “User Signup”. Start Thinking “New Tenant”. Retrieved from https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/

MLA
" » Stop Thinking “User Signup”. Start Thinking “New Tenant”." Paul Towers | Sciencx - Wednesday July 9, 2025, https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/
HARVARD
Paul Towers | Sciencx Wednesday July 9, 2025 » Stop Thinking “User Signup”. Start Thinking “New Tenant”., viewed ,<https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/>
VANCOUVER
Paul Towers | Sciencx - » Stop Thinking “User Signup”. Start Thinking “New Tenant”. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/
CHICAGO
" » Stop Thinking “User Signup”. Start Thinking “New Tenant”." Paul Towers | Sciencx - Accessed . https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/
IEEE
" » Stop Thinking “User Signup”. Start Thinking “New Tenant”." Paul Towers | Sciencx [Online]. Available: https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/. [Accessed: ]
rf:citation
» Stop Thinking “User Signup”. Start Thinking “New Tenant” | Paul Towers | Sciencx | https://www.scien.cx/2025/07/09/stop-thinking-user-signup-start-thinking-new-tenant/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.