Launch Architecture
Launch is a design intelligence platform that moves an owner from idea to buildable options in five steps: gather criteria, benchmark against historical cost, generate site-fit designs, attach costs, and compare. The design step uses generative blocking — real unit layouts on a real site that produce quantities directly tied to the benchmarking cost data. Internally, teams work across each step collaboratively. The owner sees the compiled result: options with visuals and numbers they can act on.
Auth
Launch serves three types of users: B&G employees, external stakeholders (clients, architects), and external systems like Magnus (in the future). Each needs a different way in.
B&G employees use Microsoft Entra SSO. External stakeholders use email/password or third-party providers like Google. External systems use OAuth 2.1An industry-standard protocol that lets apps request limited access on behalf of a user without sharing passwords. to call the API on behalf of a user.
Better Auth handles all of it — SSO, email/password, social providers, and OAuth provider mode.
Who logs in how
| User | Method | What happens |
|---|---|---|
B&G employee (@brasfieldgorrie.com) | Microsoft Entra SSO | Redirected to Microsoft, comes back with a session cookie. |
| External stakeholder | Email/password or social provider (Google, etc.) | Standard sign-up/sign-in. |
| External system (Magnus, AI agents) | OAuth 2.1 (Authorization Code + PKCEProof Key for Code Exchange — an extra security step that prevents stolen authorization codes from being reused.) | Redirects user through Launch's login, receives a JWT, calls the API with Authorization: Bearer <jwt>. |
The first two produce a session cookie. The third produces a Bearer JWT.
How it normalizes
No matter which path a request takes, the auth middleware produces the same shape:
CurrentUser {
userId: string
isInternal: boolean // B&G employee
isSystemAdmin: boolean
impersonation?: { // admin viewing as another user
realUserId: string
}
}isInternal and isSystemAdmin are stored on the user row in the database and pulled fresh on every request. Impersonation lets a system admin see Launch as another user for debugging — the impersonated session is forced to isSystemAdmin: false and every event is logged.
Permissions
A single person might be an admin on one org, a read-only client on a project in another, and have an override somewhere else. Every request answers: what can this user do, right here?
How it works
The system checks permissions, not roles. It never asks "is this person an admin" — it asks "does this person have project:edit in this scope." Roles are just a way to assign a bundle of permissions when someone joins.
Permissions are always checked against a specific org or project. If you're a member of a project, your project role applies. If not, it falls back to your org membership. First match wins.
Vocabulary
Every capability is a resource:action pair, typed in one place.
organization: view | edit | delete | manage
project: view | create | edit | delete | manage
member: view | invite | edit | remove
configuration: view | manage
benchmark: viewRoles
When someone joins an org or project, they get a role — a grouping of these permissions.
Org-Level
admin B&G employee managing the client
org:* project:* member:* config:* benchmark:view
member B&G employee assigned to the org
org:view project:view member:view config:view benchmark:view
viewer External client stakeholder
org:view project:view member:viewProject-Level
lead B&G project lead
project:view/edit/manage member:* config:* benchmark:view
editor B&G team member
project:view/edit member:view config:view benchmark:view
client External stakeholder
project:view member:view benchmark:viewResolution
On every request, the system queries the database for org_member, project_member, and permission_override for the user's membership in that scope. No caching, so changes take effect immediately.
Per-membership overrides (grant or revoke individual permissions) are supported but not expected day-to-day. They're there for future flexibility.
Enforcement
Each service pipes through withPolicy(...) which runs the permission check first. If it fails, the service short-circuits with ForbiddenError and the logic never runs.
withPolicy(scopePermission("project:create", { type: "org", orgId: dto.orgId })),
// compose — all must pass, or any can pass
withPolicy(all(
scopePermission("project:edit", scope),
scopePermission("member:view", scope),
))
withPolicy(any(
scopePermission("project:manage", scope),
isInternal,
))The composable policy pattern was inspired by Building a Composable Policy System in TypeScript with Effect by Lucas Barake.
Request Flow
Every client hits the same RPCRemote Procedure Call — instead of REST routes, every operation is a typed procedure call. The client calls a function by name and the types, errors, and auth are handled automatically. endpoint. Auth and permissions are handled the same way regardless of caller.
POST /api/rpc
→ AuthMiddleware → CurrentUser
→ RPC Handler
→ Service
├── withPolicy (permissions check, fails early)
├── business logic
└── repository → DB
→ typed responseThe codebase is layered so services don't depend on how requests arrive or where data is stored. The RPC layer and database layer are both replaceable without touching them.
Core Technologies
Next.js — web app and server runtime.
Effect-TS — typed errors, composable services, dependency injection.
TanStack Query — data fetching, caching, and invalidation on the frontend.
Drizzle ORM — type-safe database schema and queries.
Better Auth — SSO, email/password, social logins, OAuth provider.
Systems & Attack Surface
| System | Sensitivity | Data | Access |
|---|---|---|---|
| PostgreSQL (Neon) | High | PIIPersonally Identifiable Information — any data that can identify a specific person (name, email, etc.), auth, all state | Connection string (TLS) |
| Entra ID | High | Identity, SSO tokens | OAuth 2.0 client creds |
| Vercel (secrets) | High | All credentials | Team access, scoped envs |
| Databricks | High | Proprietary cost data | PATPersonal Access Token — a long-lived API key that authenticates as a specific user or service account., read-only |
| Azure Blob | Medium | Project files | Key → SASShared Access Signature — a time-limited, scoped URL token that grants temporary access to a specific blob without exposing the master key. URLs |
| OpenAsset | Low | Marketing images | API token, read-only |
Repo: github.com/BG-Innovation/launch
Demo: alpha.launch.brasfieldgorrie.app (this is the alpha version which does not implement all the permissions discussed above)