sample 04/in development
Atlas
A sales CRM that became a management system for small businesses, shaped to each company's industry.
- Next
- Prisma
- Zod
- Turbo
01
Context
Atlas started as a CRM for a sales team: a status pipeline, goals, email templates and sending through each seller's Google account.
In August the product changed direction. It became a multi-tenant platform where each business, such as a restaurant or a barbershop, gets a panel with the modules for its industry.
02
Starting point
The first version served a single sales team, running on demo data.
3
roles: seller, manager and admin
600
simulated accounts in demo mode
1
company per installation
03
Timeline
56 commits in the project history. These are the milestones.
Jul 02, 2026
First version
A sales CRM with one portal per role and a demo mode with simulated data.
Jul 09, 2026
Gmail integration
OAuth connection to the Google account, encrypted tokens, send limits and a kill switch for the whole integration.
Jul 10, 2026
Sign in with Google
Individual identity per Google account, alongside password login.
Aug 08, 2026
Pivot to multi-tenant
Foundation rebuilt: company and industry inside the signed session, company sign-up with its owner and the first complete module, Customers, following the repository, service and route pattern.
Aug 09, 2026
Operations modules
Cash flow, catalog, service records, stock and revenue forecast.
Aug 11, 2026
CRM and analytics
Customer segments with privacy-law consent and per-industry analytics panels.
Aug 15, 2026
Real database
Moved from demo mode to Postgres, with the old schema's data backed up before the switch.
Sep 08, 2026
Alpha-only access
Login closed to the alpha test group.
04
Decision map
Reconstructed from the commit history, the README and the project documentation.
01
Rebuild the foundation, not the whole product
Authentication, the repository pattern and the design system stayed. The sales domain left and a generic business model came in.
02
The company comes from the session
Every business route reads the company from headers the middleware injects from the signed session. The browser has no way to ask for another company's data.
03
Industry is configuration
A registry in code defines which modules each industry turns on and what things are called. A new industry is a new entry, not a new table.
04
One data model for everyone
Customers, catalog items, service records, ledger entries and stock serve every industry, with a flexible field for what's specific to each.
05
Demo without infrastructure
A repository factory switches between in-memory data and Prisma with Postgres. The whole product can be evaluated without setting up any database.
06
An alpha with minimal scope
The alpha serves only business owners, one per company, with no team management. Fewer live routes means less attack surface.
05
Architecture
The same five layers as this page, applied to the project.
000 m/interface
- Panel shaped by industry
- Design system with shadcn/ui
- Charts with Recharts
030 m/route
- Role-based middleware
- HMAC-SHA256 signed session
- Client-forged headers discarded
060 m/service
- Per-module services
- Zod validation
- Industry and module registry
090 m/repository
- Repository factory: memory or Prisma
- Every query scoped by company
- Soft delete
120 m/persistence
- Postgres on Supabase
- Passwords with scrypt
- OAuth tokens encrypted with AES-256-GCM
06
Code
An excerpt from the private repository, just to show the layer at work.
The company never comes from the browser
Every business module starts here. The company id comes from the signed session, and without it the route doesn't go on.
// Lê identidade + tenant dos headers injetados pelo middleware (assinados
// via cookie HMAC — o client não forja). Use em route handlers.
export async function getApiContext(): Promise<ApiContext | null> {
const h = await headers();
const userId = h.get("x-user-id");
const role = h.get("x-user-role") as Role | null;
if (!userId || !role) return null;
return { userId, role, companyId: h.get("x-company-id") };
}
/**
* Contexto que EXIGE empresa (módulos de negócio). SK_ADMIN sem empresa
* selecionada recebe 400 — a visão cross-tenant tem rotas próprias (/api/sk).
*/
export async function requireCompany(): Promise<RequireCompanyResult> {
const ctx = await getApiContext();
if (!ctx) {
return { error: NextResponse.json({ error: "Não autorizado" }, { status: 401 }) };
}
if (!ctx.companyId) {
return {
error: NextResponse.json(
{ error: "Nenhuma empresa no contexto." },
{ status: 400 }
),
};
}
return { ctx: { ...ctx, companyId: ctx.companyId } };
}The same item, different names per industry
A single table in the database, with each business's language on screen. That's what lets new industries come in without touching the data model.
// Terminologia do catálogo por setor (o mesmo CatalogItem se chama "prato"
// num restaurante e "serviço" num salão).
export function catalogTerms(sector: Sector | null): CatalogTerms {
switch (sector) {
case "CABELEIREIRO":
return { title: "Serviços", singular: "serviço", plural: "serviços", addLabel: "Novo serviço" };
case "RESTAURANTE":
default:
return { title: "Cardápio", singular: "item", plural: "itens", addLabel: "Novo item" };
}
}07
Outcome
2
live industries: restaurant and barbershop
11
tables in a model shared by every industry
3
roles: platform team, owner and staff
Current status
In alpha testing with business owners, starting with barbershops.