Role Testing

S
Swathi
May 26, 2026
3 min read

Welcome to OrbitNexa — a modern enterprise SaaS platform that brings clients, projects, finance, and HR into one place. This post walks you through the core ideas in under five minutes.

Why we built it

Six months ago, our team was juggling four separate internal tools — one for clients, another for finance, a third for HR, and a hand-rolled job board on top of all of it. Switching between them ate more hours than the actual work. So we replaced them.

Team during a planning sprint
Our team during a planning sprint

Three constraints we set on day one

  1. One auth provider. No second login screen for an internal feature, ever.
  2. One data model. Clients, projects, invoices, employees — all share the same key conventions.
  3. One design system. New screens reuse existing components or we don't ship them.

What we shipped

Twelve modules in 90 days — clients, projects, milestones, invoices, expenses, payroll, leaves, attendance, employees, tasks, content, and analytics. All Cognito-authenticated, all served from a single Next.js admin app, all reading from one DynamoDB table.

  • Clients & projects — onboarding, milestones, attachments
  • Finance — invoices, expenses, approvals, GST tax engine
  • HR — employees, leaves, attendance, payroll runs
  • Content — blog, case studies, careers, all approvals-gated
The fastest internal tool is the one you don't have to context-switch into.

The architecture in 30 lines

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { requirePermission } from "../shared/rbac";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export async function appsyncHandler(event: AppSyncEvent) {
  const caller = callerFromIdentity(event);
  if (!caller.sub) throw new ForbiddenError("unauthenticated");

  await requirePermission(caller, "content.blog.manage");

  return invokeAsRest("POST", "/content", event.arguments.input);
}

What didn't work

Two things we tried and dropped. First: a generic "low-code form builder" for non-engineers. It bloated the schema and nobody used it after week two. Second: per-feature React state libraries — Zustand here, Jotai there, Redux in one corner. We standardised on React Query for server state and component state for everything else.

What we'd do differently

  • Start with permissions on day one — retrofitting RBAC costs 3× more.
  • Wire the notification bell before adding the third approval flow, not the eighth.
  • Write the migration script before the seed data, not after.

If you're starting an internal platform from scratch, here's the only real advice: decide what you're not building. The rest is just typing.