Decisions
Standing calls, so they don't get re-litigated. Newest first. A decision here is settled until something in "revisit when" actually happens.
Higgsfield MCP auth — device code flow, not bearer
Status: established by probing the endpoint · Date: 2026-08-19
The MCP server at mcp.higgsfield.ai is the richer surface: 86 tools including Marketing Studio, apps_search/describe/invoke, presets, reframe and upscale. The REST API exposes far less and each capability would need its own integration.
What the probe established, in order:
Authorization: Bearer <API_SECRET>returns 200 and a working MCP session.initialize,tools/listandmodels_exploreall succeed.- But it is not a user session. Any account-bound call — generation, cost preflight, Marketing Studio — returns *"session expired or no longer valid, re-authorize the connector"*. The API secret authenticates the transport and nothing else.
- Higgsfield's own discovery document names two flows.
authorization_code_pkcefor clients that can receive a redirect (Claude Code, Claude.ai), anddevice_codeviafnf-device-auth.higgsfield.ai, described as being for clients that *"cannot receive authorization-code redirects and should ask the user to complete verification out-of-band"*.
Device code is the answer for a headless box. The VPS prints a URL and code, a human approves once, and the box receives tokens. offline_access is a supported scope, so a refresh token comes with it — authorise once, stay connected.
This means no Anthropic Managed Agents vault is required to solve it, and no support ticket. It also means the MCP path draws on the subscription balance rather than the separately-funded API platform wallet, which is where the credits actually are.
Do not conclude from a 200 on initialize that authentication is solved. Catalogue calls and account calls authenticate differently here.
Revisit when: Higgsfield publishes first-class server credentials, which would remove the device-code step entirely.
HR and admin — deferred, and payroll will be bought not built
Status: deferred, not rejected · Date: 2026-08-19
The portal will eventually cover HR and admin for staff. Split three ways, because they are three different problems:
Payroll — buy. Singapore payroll means CPF rates varying by age and residency, wage ceilings, IR8A and IR21 filing, and statutory itemised payslips. The rates change and someone must track them permanently; errors are penalties from MOM or IRAS, not bugs. Talenox, Payboy and HReasily handle this at roughly $5–8 per employee per month. Building it would mean owning statutory compliance forever to save lunch money.
Leave and claims — probably buy. Simple to build and low risk, but the payroll tools bundle it. Building means maintaining a second system to avoid a feature already paid for. Only worth it if having everything in one place is worth owning it.
Utilisation and client profitability — build, eventually. This is the part nothing off-the-shelf does, because it needs delivery data the portal already holds: which person worked on which client, hours consumed against retainer value, who is at capacity. Generic HR tools cannot answer it; they have no concept of a client, an angle or a creative. Most agencies discover a client was unprofitable only at the end of the engagement.
Hard constraint when this is built: if client tenants ever get logins, salary data must not share a schema with anything a client can reach. Separate Postgres schema at minimum, separate database preferably. An RLS bug exposing a message angle is embarrassing; the same bug exposing payroll is existential. The only legitimately shared surface is identity, which profiles already provides.
Nothing to do now. No schema changes are needed to keep this open.
Revisit when: headcount makes capacity planning guesswork, or a retainer's profitability is genuinely unclear.
Atlas is the host, not a tenant
Status: implemented (0002_tenancy.sql)
Multi-tenancy is not built, only made addable. Atlas staff reach every tenant; a client tenant sees only its own rows. Offboarding flips one boolean — access stops, data is retained.
Revisit when: a client actually needs a login.
Infrastructure as code, not a dashboard
Status: implemented (infra/provision.sh, docker-compose.yml)
Coolify was the original plan and was dropped: its configuration lives in Coolify's own database, and click-configured infrastructure does not version-control. For a stack intended to be repeated across clients, standing up the next environment has to be a script run, not a memory test.
Revisit when: a client needs a self-service dashboard — Coolify can sit on top, with the compose stack still the source of truth.
No LLM in the high-volume path
Status: implemented
Agents where volume is low and judgment is high; code where volume is high and the path is fixed. Creative generation has no model in it at all. The "master agent" of the standard orchestration diagram is deliberately absent — a model routing a predictable pipeline adds cost and non-determinism for nothing.
Revisit when: a pipeline appears whose steps genuinely cannot be predicted. Resolving a hard escalation is the first real candidate.
Build as superuser; add roles when the first non-founder logs in
Status: decided, not implemented
Every feature is built as though the person using it may see and do everything. Roles and permissions are deliberately deferred.
Three reasons:
There is no identity to attach a role to. Auth is off by explicit decision. A permission system without authentication is decoration — it gates a UI that anyone can bypass by calling the route directly. Roles are meaningless until there is a login, so building them first is building the second half of a feature.
Permission code is the highest-churn code in a moving system. Every new feature adds rules to the matrix, and every change to a feature changes its rules. Building the matrix now means rewriting it with each of the features in the vision sequence — the single largest source of avoidable revision cost available to us right now.
The expensive half is already paid. What makes permissions painful to retrofit is not the checks, it is the data model: if rows cannot express who owns them, no amount of later work fixes it. Ours already can — tenant_id on tenant-scoped tables, client_id throughout, 52 RLS policies, and a profiles table carrying roles. The structure is in place and unused, which is exactly the right state.
What must not be given up while deferring:
- Every query keeps going through
services/. That is the one place a scope filter gets added later; logic in a route handler is logic that has to be re-secured twice (seeCLAUDE.md). - No denormalising data across tenant or client boundaries, however convenient.
- No feature whose usefulness depends on seeing everything at once in a way that cannot be scoped.
Revisit when: the first person outside the founding team needs a login. That is the concrete trigger, not a date. Atlas Brain's owner/employee split is the first feature where roles are genuinely load-bearing rather than cosmetic, and it sits last in the sequence — which is consistent: by the time it is built, auth will have had to land anyway.
The application role cannot delete
Status: implemented (migration 0008, both environments)
Thirteen tables cascade from clients and the application was connecting as postgres, so a single delete from clients would have destroyed a client's entire history — every verified fact, every citation, every raw transcript, every angle, every creative — against daily backup granularity. Nothing in the UI does that, but the capability was one query away in a system meant to be driven by an agent over MCP.
atlas_app is granted select, insert and update, and is never granted delete or truncate. Removal now requires a deliberate administrative connection, which is the friction it should always have had. Default privileges are altered too, so tables from future migrations inherit the same shape rather than silently arriving deletable.
BYPASSRLS is set and is the part that would have taken the portal down if missed: all 56 policies gate on auth.uid() is not null, there is no auth, and a role subject to RLS reads zero rows from every table. postgres was bypassing RLS implicitly; this makes the same bypass explicit while changing only the destructive privilege.
Migrations and pg_dump use ADMIN_DATABASE_URL. The backup mattered more than the migrations: pg_dump run as a non-owner produces an incomplete dump without failing, and a backup that looks like it worked is worse than one that errors.
Revisit when: authentication lands — BYPASSRLS comes off then, and the policies start doing the work they were written for.
Higgsfield first for anything creative
Status: standing instruction from Alex
Before building any creative capability, check whether Higgsfield already does it. Image generation, video, editing, layer separation, clipping, upscaling, reframing, background removal, outpainting, motion, virality prediction, video analysis, voice, and a marketplace of apps — all exposed over MCP.
This is not a preference for convenience. It is the resourcefulness constraint applied where it pays most: a purpose-built creative studio has design capability we would need many months and a design team to approach, and every hour spent reimplementing it is an hour not spent on the parts nobody else can build for us — the knowledge base, the guardrail, the insight synthesis, the taste rules.
Known surfaces worth reaching for before writing anything:
| Need | Higgsfield surface |
|---|---|
| Static ad rendering | Marketing Studio, 42 ad formats |
| Visual treatment | ~60 viral presets |
| Video ads | 26 video presets, UGC modes |
| Editing a finished creative | Edit Layers — splits a flat creative into editable layers |
| Reframe, upscale, outpaint, remove background | dedicated tools, better than regenerating |
| Concept extraction from a real ad | ad_reference |
| Brand consistency | brand_kit — hex colours with usage, fonts, logo |
| Clipping, analysis, virality | personal_clipper, video_analysis, virality_predictor |
What we never delegate: what is true, what we are arguing, whether it is allowed, whether it is good, and verification of what came back. See higgsfield-as-art-director.md.
Revisit when: a capability is genuinely absent, or when a guardrail cannot be closed by reading the output back.
The portal is the source of truth; Drive is downstream
Status: ruled by Alex, 20 Aug 2026
The August 2026 client import — 32 records created from the Drive "01. Clients" folder — was a one-time legacy port, not a pattern. From here the direction reverses: clients are created in the portal, and their Drive folders are created *from* the portal via the API (once write access via Shared Drive membership exists). Nothing syncs Drive → portal except the read-only notes sweep, which extracts knowledge rather than mirroring files.
Two standing constraints from the same ruling:
- Never delete or move anything in Drive. At most copy. The team navigates by those folders, and a moved file is a lost file as far as they are concerned. Enforced today by scope (
drive.readonly) and later by Shared-Drive Contributor membership, which can add but not delete. - Archived clients in Drive were not imported. The portal's roster is active engagements; the archive stays where it is.
Revisit when: folder automation lands — creation order becomes portal → Drive, and this entry is the reason why.
21 Sep 2026: sales runs on GHL plus the portal, with a defined split
Decision (Alex): keep GHL. Appointment setters work in GHL for the inbox (WhatsApp and email), dialling, and workflow decisions and execution. The sales team (Jun Kai) works in the portal, which is an editable view of the GHL data and the feedback loop: status, judgement tags such as low-quality and disqualified, team fields and notes, written to GHL through one audited path so workflows act on the sales team's judgement rather than against it.
Why not rebuild GHL in the portal: the parts actually used are the inbox and booking. A WhatsApp Business inbox and email deliverability are commodity products because they are hard to get right, and a setter lives in them all day. Everything else (record, fields, tags, notes, list, filters) is already in the portal. Revisit the inbox only once conversation volume through GHL is known.
Consequences: GHL is the system of record; the portal never holds a sales fact GHL does not. Portal to GHL is immediate (write in the request, re-read, mirror). GHL to portal is a workflow webhook plus a 3-hourly full sync. Identity fields (name, phone, email, company) are never written from the portal. Supersedes the 17 Sep "portal is a view only" note in docs/knowledge/atlas-sales-pipeline-setup.md.