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

UserMethodWhat happens
B&G employee (@brasfieldgorrie.com)Microsoft Entra SSORedirected to Microsoft, comes back with a session cookie.
External stakeholderEmail/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:     view

Roles

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:view

Project-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:view

Resolution

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 response

The 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

SystemSensitivityDataAccess
PostgreSQL (Neon)HighPIIPersonally Identifiable Information — any data that can identify a specific person (name, email, etc.), auth, all stateConnection string (TLS)
Entra IDHighIdentity, SSO tokensOAuth 2.0 client creds
Vercel (secrets)HighAll credentialsTeam access, scoped envs
DatabricksHighProprietary cost dataPATPersonal Access Token — a long-lived API key that authenticates as a specific user or service account., read-only
Azure BlobMediumProject filesKey → SASShared Access Signature — a time-limited, scoped URL token that grants temporary access to a specific blob without exposing the master key. URLs
OpenAssetLowMarketing imagesAPI 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)