ConnectIQ Internal · development strategy
Document
Development strategy
Status
Decided · 1 open
Scope
How anything ships
Owner
Platform

Development strategy · the machinery

How anything ships. Not what ships, or when.

The delivery plan launches one domain at a time — a milestone, its scope, its exit criterion, the decisions it is blocked on. This page is the part that does not change between milestones. Environments, branching, testing, hosting, and the guardrails that keep a code-generating assistant inside its own domain.

It is separate because it has a different half-life. The delivery plan is consumed and finished; M5 happens once. This is set up once and then lived in for the whole of phase 1 and phase 2 — and every decision here is paid for on every milestone rather than on one.

The tiers four
  • Dev Lovable · per domain
  • Staging untested, viewable
  • Production Vercel · main
  • Rehearsal migration only
Supersedes plan
  • ADR-06 no staging → three tiers
  • DR-28 hosting → Vercel
  • §19 testing stance
  • Email provider open → Resend

01 · The split

Why this is not in the delivery plan

The delivery plan answers what ships, in what order, and what done means. It is a sequence: M1 through M7, then S1 through S5, each with a scope, an exit criterion and a list of decisions it is blocked on. Read it once per milestone.

This page answers how anything ships at all — the environments a change moves through, the branching model, what gets tested and by whom, where production runs, and the guardrails that keep Lovable inside one domain. None of it is milestone-specific. Set it up before M1 and it serves every milestone after.

Delivery plan owns

  • Scope — what is in phase 1 and what is deferred
  • The milestone sequence and its dependencies
  • The data model and the entity-to-domain map
  • Migration and cutover
  • The register of outstanding decisions
  • One build prompt per milestone

This page owns

  • The stack, and why each part was chosen
  • Environments and the branching model
  • The Lovable project, and how several people share one
  • Supabase branching, cost and seeding
  • Testing — what is tested, where, by whom
  • Hosting and the CI/CD path to production
  • Email — provider, sending domain, and both send paths
  • Guardrails, the knowledge file, shared-file discipline
  • The quality bar every milestone is held to
Precedence, unchanged

The repo rule is model → architecture → plan → screens. This page sits alongside the plan, not above it: the architecture remains authoritative on anything structural — domains, table ownership, naming and cross-domain access — and nothing here may contradict it. Where this page and the delivery plan disagree, this page wins on method and the plan wins on scope, and §12 records what changed.

02 · Stack

Boring where it can be, deliberate where it can't

The prototype established a stack that works. This keeps the parts that carried their weight and re-decides the parts that were chosen by the prototyping tool rather than by us. Moved here from the delivery plan, with the hosting rows now settled.

LayerDecisionConfidenceNote
DatabasePostgres (Supabase) with row-level security on every tableSettledMulti-currency money, dated periods and forecast roll-ups all want a relational engine. RLS makes the permission model enforceable at the data layer rather than in the client.
AuthSupabase Auth — email + password, Google OAuth, no anonymous sign-inSettledRoles live in plat_user_roles and are checked by a SECURITY DEFINER function. Never on the profile row.
BackendServer functions in the app, not a separate serviceSettledOne deployable, internally separated by domain prefix — the separation rules are in the architecture.
Cross-domain effectsOutboxplat_domain_events, written in the same transaction as its cause, drained by a scheduled dispatcherSettledBinding in the architecture. M1 work, not a retrofit.
FrontendReact + TypeScript strict, file-based routing, query cache with loader-fetched routesSettledForms on a schema validator shared with the server. Charts from one library, icons from one family.
StylingSemantic tokens from style-kit/tokens.json. No raw palette classes in feature codeSettledEnforced by lint rule, not by review discipline.
Code generationLovable, one project per domain, working inside a fixed folder structureSettledSee §04 and §09. The structure exists before Lovable is asked to build into it.
Hosting — appVercel, connected to the production branch only, serving connectiq.giant-pumpkin.comSettledCloses the plan's open hosting question. Chosen for dev speed, staging previews and reduced devops surface. See §08.
Hosting — databaseManaged Supabase, Pro planSettledPro is not optional — branching for preview environments requires it. See §06.
Background workQueue tables in Postgres, drained by a scheduled workerSettledEmail sending, Airtable sync, the event dispatcher and nightly aggregates all use the same pattern. No second infrastructure component.
EmailResend — transactional only, templated with React Email, suppression list fed by webhookSettledFirst-party integration on all three sides of the stack. Covers both send paths from one account. See §09.
Docs siteStatic HTML on the existing droplet, deployed from this repoSettledWhat you are reading. No build step, no framework, no dependency to keep current.
The runtime constraint survives the hosting decision

Vercel's serverless functions carry the same restriction the plan flagged against an edge runtime: no native binaries, no child processes, no long-lived TCP connections. That still rules out several PDF, image and spreadsheet libraries the quote print view and the import module might otherwise reach for. Choosing Vercel answers where, not what is allowed — the print view still has to be built against that limit, and it is still cheaper to know before M5 than during it.

03 · Environments

Three tiers, and what each one is allowed to be

The delivery plan originally ran two environments and argued against a standing staging tier. That argument has been overturned: with a code-generating assistant producing whole features, there has to be somewhere untested code can be seen and exercised before it reaches production, and Lovable can view a branch directly. See ADR-07.

Drawn once, in §05

The flow these tiers form — two feature branches, one staging branch, a human gate, then main — is Fig 1. It is not repeated here; the table below carries what a diagram cannot, which is the branch, database, audience and lifetime of each tier.

TierBranchDatabaseWho sees itLifetime
DevelopmentFeature branch, per domainSupabase preview branch — starts empty, seeded from seed.sqlThe developer, and LovableDeleted when the work merges. Unused branches cost money.
Stagingstaging — persistentPersistent branch. A copy of production, unmodified — see §06Anyone, including reps for early feedback. Viewable from Lovable.Standing.
Productionmain — persistentProduction project, real data, nightly backupsEveryoneStanding.
Production, pre-launchmainThe real thing, empty — then loaded, wiped and reloaded per import attemptWhoever is running the importA phase, not an environment. Ends the moment a real user touches it.
The migration rehearses in production, because production is spare until it isn't

There is no fourth environment. Until the parallel run begins, production is empty and nobody is working in it — which makes it the most accurate possible rehearsal target and costs nothing extra. It has the real permission model, the real RLS policies and the real configuration, none of which a copy reproduces exactly.

This works because of one line, and it stops working the moment the line is crossed:

  • Wipe-and-retry is permitted until the point of no return. Import, read the error report, fix the mapping, wipe, import again. The gate is unchanged from the original plan — at least two clean runs, and a rep confirming their own accounts look right.
  • The point of no return is the first real user action, not a date. The instant someone enters an opportunity that did not come from the import, production can never be wiped again and every later problem is a forward-only repair. Declare that moment out loud.
  • Back up before every attempt. Cheap before launch, and it turns a bad run into a restore rather than an afternoon.
  • Disable outbound email during import runs. Migration creates owners and members; if that triggers invitations, real staff get invited repeatedly by a rehearsal. Suppress sends, then re-enable for the real one.

After the point of no return, the parallel run is the probation period — both systems live, HubSpot still readable, and the fallback if something is badly wrong. That is what the monitoring in step 6 is for, and it is the last moment a retreat is cheap.

A preview branch cannot serve as the rehearsal either

Supabase preview branches are data-less by design — no production data is copied, deliberately, to protect it. That is exactly right for feature work and exactly wrong for a migration rehearsal, which is the other reason the rehearsal runs in production rather than on a branch.

04 · Lovable projects

One repository, one project, a branch each

Lovable maps one project to one repository, strictly. A repository cannot be connected to two Lovable projects. Since ConnectIQ is one repository — one deployable, one Postgres separated by prefix — it follows that there is exactly one Lovable project, which supersedes the "DEV Project #1, DEV Project #2" arrangement in the earlier workflow note — that predates the single-repository decision and does not apply.

That is a constraint on projects, not on people. One project holds as many collaborators as it has seats, and several can work in it at once. What follows below is how they stay out of each other's way.

Several people, a branch each, never two on one feature

This is the working model, and it comes straight from the source walkthrough: multiple developers at any given time, working feature by feature, each editing only what their feature needs and not touching anyone else's code. Isolation is the branch plus the domain folder — not a separate workspace.

Every branch is cut from staging and merges back into staging by pull request. Two people never share a feature branch unless they are deliberately pairing on it.

The one thing a single project cannot do

Lovable's active branch belongs to the project, not to the person. Switch it and everyone in that project switches with you. Several people can be in Lovable at once — it supports real-time multiplayer with visible cursors — but they are all on the same branch.

So the default for two people on two features is: one drives Lovable on their branch, the other works theirs in a normal editor. Both still merge to staging the same way. Lovable is the generation tool in this setup, not a per-person workspace.

And if two people are in Lovable on one branch together, divide by file rather than by intent — simultaneous prompts against the same component can overwrite each other.

Two things to verify before relying on this

Branch switching is a Labs feature — Settings → Account → Labs. Turn it on before M1 rather than discovering it is off.

And confirm by test that two-way sync behaves on a non-default branch. Lovable's branch switching is documented as syncing whichever branch is active, but at least one secondary source claims two-way sync only applies to the default branch. Those cannot both be true, and the whole promotion path assumes the first. Ten minutes on a throwaway branch settles it.

A branch, not a fork

Git makes this a real distinction and it decides the architecture. A branch is a line of work inside the same repository, merged by a pull request within it — that is the model here. A fork is a separate copy of the repository, and under Lovable's one-to-one rule a fork would need its own Lovable project, which puts you straight back into two repositories, two package.json files and a deployable the architecture does not describe.

A branch per feature. Never a fork.

Domains are folders and prefixes, not projects

The source document describes dividing development "as domains/microservice". The architecture explicitly rejected microservices in favour of one deployable separated by eleven locked prefixes — cl_, com_, plan_, ops_, inv_, sub_, sup_, mon_, int_, rpt_, plat_. With a single project, the isolation that keeps work from colliding is the folder boundarysrc/domains/<domain>/ — and the prompt discipline in §10, not a separate workspace.

Caution — Lovable is not repo-first

Lovable cannot import an existing GitHub or GitLab repository. The flow is Lovable project → Git repo, never existing repo → Lovable project. Do not treat it like a normal repo-first development environment.

This reorders M1. The delivery plan's first milestone opens with "repository, CI, and the three lint rules" — but the repository cannot be created first and then attached. The repo is born from the Lovable project, and CI, lint rules and the migration dry-run are added to it afterwards. Sequence M1 accordingly, or the first afternoon is spent discovering it.

Connecting a project creates the repository and starts a two-way sync automatically. Lovable syncs one active branch at a time, with branch switching and creation supported — and its own guidance warns to use branching carefully and not to delete a branch before switching back to main, because sync problems follow.

05 · Git branching

The most fundamental feature, used plainly

Nothing clever. Feature branches per domain, an integration branch where untested code can be seen, and a main branch that is only ever tested code.

01 · DEVELOP · ONE REPO, ONE LOVABLE PROJECT 02 · INTEGRATE 03 · VERIFY 04 · RELEASE Domain A FEATURE BRANCH From staging · own preview DB Domain B FEATURE BRANCH From staging · own preview DB PR / MERGE Staging SHARED · MERGED, NOT RELEASED Work from both branches is combined and checked as one application before release. VIEWABLE IN LOVABLE QA gate A PERSON PASS Production MAIN · VERCEL Tested, approved code. Auto-deploys on every merge to main. FAIL → FIX Issues go back to the branch that caused them — never patched on staging. Promotion path FEATURE STAGING MAIN Never deploy untested feature code straight to production.
Fig 1 — The promotion path, and the only route to main. One repository, one Lovable project, a branch per domain cut from staging. Work is combined on staging, checked there as one application, and released only after a person approves it. Also the environment map: each feature branch carries its own Supabase preview database, staging and main run on persistent ones.
Domain Afeature branch · cut from staging · own preview database
Domain Bfeature branch · cut from staging · own preview database
Staginguntested code · viewable from Lovable
Productiontested code · main · deployed by Vercel
The gate between staging and main is a person, not a check

Someone opens staging, exercises the features, and decides. Only then does the staging-to-main pull request get merged. Generated tests and CI run underneath that decision and inform it — they do not replace it, because the thing being judged is whether a release is coherent, not whether it compiles.

This is the same shape as the delivery plan's quality bar, which ends every milestone with demoed to a real user of that surface. One person, named, deciding — the same defence the migration cutover uses against drift.

Rules

  • Branch from staging, never from main. Staging carries the latest in-progress work; a branch cut from main starts behind everything already merged and buys a conflict for nothing.
  • A merge conflict means the branch was too big. Treat it as the signal, not the problem — one feature per branch, merge, then go again. Splitting by page or by screen is the usual grain.
  • Nothing merges into main except staging. There is no path from a feature branch to production, however small the change.
  • Short branches. Merge behind a flag where a feature spans releases.
  • Schema changes are numbered SQL migrations committed to the repo — including the ones Lovable generates. The repo stays the source of truth; the Supabase console is never the place a change is made.
  • Every migration that creates a table creates its RLS policies in the same file.
  • CI runs type-check, lint (tokens and cross-domain imports), unit tests, the RLS-policy check and a migration dry-run.
  • Merge to main deploys. Rollback is a redeploy of the previous commit.
  • Switch back to main before deleting any branch.

Documentation deploy

This site deploys from the same repository by rsync to the droplet on every push to the default branch. It carries no application code and no secrets, so it ships continuously without ceremony.

06 · Supabase branching

The database branches like the code does

The reason this whole model holds together: Supabase can branch the database the same way git branches the code. A feature branch gets its own database, so a migration written against it cannot damage anything else.

Persistent branches

staging and production. Always on, seeded, and paid for continuously.

Preview branches

Feature work. Created with the branch, deleted with it. Usage-billed by the hour.

Cost

Branches are not free forever. Each preview branch is a separate environment and you pay for its compute, disk, egress and storage. A branch on the default Micro compute size starts at US$0.01344 per hour, with no fixed fee per branch.

Roughly
≈ $0.40

One feature branch alive for 30 hours.

Roughly
≈ $9.70 /mo

One always-on staging branch, 720 hours.

Required
Pro plan

Branching for preview environments needs it. GitHub integration and CLI deployment work on any plan.

Watch
No credit

Compute credits do not apply to branching. The monthly credit that offsets your main project does not offset branches.

So the cost is manageable — with one discipline

Delete unused preview branches. The bill is not the branch, it is the branch nobody closed. At roughly ten dollars a month per always-on environment, a handful of forgotten feature branches quietly doubles the database line.

Data is not copied by default

New branches start empty. No production data is copied, deliberately — it is how Supabase keeps sensitive data out of environments that multiply. If you want test data, that is a seed.sql file, and it is a real deliverable rather than an afterthought.

Two operational details worth knowing before they bite

A preview branch is seeded once, at creation. Reseeding means deleting and recreating the branch — in a pull-request flow, closing and reopening the PR. Plan the seed file to be idempotent and complete rather than something you patch afterwards.

And the seed has to carry enough to exercise the domain: a quote needs a company, a contact, a product with a resolvable price in the quote's currency, and a seller entity, or the wizard cannot be opened at all. The seed file is a phase 1 deliverable in M1, not something that appears at M5.

Data per tier

Every tier holds different data, and the differences are deliberate. Two artifacts, not one — a seed file is small, synthetic and version-controlled; a production-shape dataset is a copy operation with a cadence and an access question. Conflating them is how a repository ends up with customer records in it.

TierDataWhere it comes fromRefresh
Feature branchDummy. Enough to exercise the domain being built, nothing more.seed.sql, committed to the repoOn branch creation. Reseeding means recreating the branch.
StagingThe same data as production. Not scrubbed, not subsetted.A restore from a production backupSet once, then allowed to drift. Close enough for QA, not a mirror.
ProductionReal.The migration, then the businessLive. Nightly backups.
Production, pre-launchReal. Duplicate companies, malformed emails, orphan contacts — the defects are what is being tested.A dated HubSpot exportWiped and reloaded per attempt, until the point of no return.
Why staging is not sanitised

Because there is nobody to protect it from. The team is two people, both of whom already hold full access to the same records in HubSpot and Airtable. Development, staging and production are reachable only by that pair. Scrubbing a copy of data everyone can already read in the source system protects nothing and costs a script that has to be maintained, correct, and re-run on every refresh.

It also improves the QA signal. Real records carry the duplicate companies, the odd characters in addresses and the contacts with no email that synthetic data never produces — the same defects the migration is being designed against.

Two things that do not follow from team size

Lovable is a route the source systems do not have. Staging is viewable from Lovable, so real customer records render inside a third-party tool. Looking at them is no different from looking at HubSpot — but pasting an error containing a customer's details into a prompt is, because that goes to a model. Not a reason to scrub; a reason to keep the distinction in mind.

This decision has an expiry condition. It holds because everyone with access already has production access. The moment that stops being true — a contractor, a designer, a QA hire, anyone who would not otherwise see customer data — the reasoning collapses and staging needs scrubbing or subsetting. Revisit on the third person, not on a schedule.

Staging cannot have production-shape data until production exists

There is no production data until M7 loads it. So for the whole of M1 to M6, "a copy of production" has nothing to copy from — and the milestone that needs realistic data most is M6, where a manager judges whether forecast numbers survive scrutiny. A handful of synthetic opportunities will not test that.

The source is available earlier than it looks. The delivery plan already starts import work as soon as M2 lands, because profiling surfaces data-quality problems while the schema is still soft. That dated HubSpot export, scrubbed and loaded through the import module being built anyway, is what populates staging during the build — and it exercises the import pipeline as a side effect. One artifact, two jobs.

After cutover, staging takes a single copy from real production and then drifts, as decided. With no scrub step in the way, that refresh is a plain restore — so if drift ever becomes a problem, repeating it is cheap.

07 · Testing

Three layers, and the one that gets real coverage

The delivery plan originally declined a broad end-to-end suite, on the grounds that at this team size the maintenance cost outruns the value. That reasoning assumed humans writing the tests. With Lovable generating and maintaining them, the cost side of that trade changes, and the stance changes with it — see ADR-09.

Generated by Lovable

  • Browser testing — one pass per epic, over the whole flow. This is the layer the old plan refused; it is affordable now because it is not hand-written.
  • Frontend testing — component and page behaviour.
  • Backend testing — server functions, including edge functions.

Written and owned by us

  • Money maths — totals, discounts, VAT inclusive and exclusive, term multiplication, currency conversion.
  • The multiplication — licence count, store count, the month-by-month series, against ten real historical deals.
  • Permission resolution — a matrix per role, asserted against the database.
  • Import mapping — fixtures including the malformed rows found in profiling.
  • Sync and event idempotency — the same payload or event twice changes nothing the second time.
The division that matters

Generated tests prove the application still works. Hand-written tests prove the numbers are right. Only the second kind is worth arguing about in review, and only the second kind should ever block a merge on its own — a browser test that goes red because a button moved is information, not a defect.

08 · Hosting & CI/CD

Production is the only branch Vercel knows about

The production repository is connected to Vercel, and nothing else is. That gives full control over the build, environment variables and the production infrastructure, isolated from any other tool in the chain.

ConcernWhere it lives
Production domainconnectiq.giant-pumpkin.com — a subdomain, which is what keeps application email off the Workspace root. See §09.
Build and deployVercel, triggered by merge to main
Preview deploysVercel, per branch — how staging is looked at without a second host
The release previewOpening the staging-to-main pull request makes Vercel deploy a preview of exactly what is about to go live, separate from what users currently see. This is the QA gate's last look, and it catches what crept in while everyone was thinking about the release rather than the code.
Environment variables and secretsVercel project settings and the Supabase secret store. Never the repo.
RollbackRedeploy the previous commit
Database migrationsNumbered SQL files in the repo, applied through the Supabase GitHub integration
Scheduled workQueue tables drained by a scheduled worker — email, sync, the event dispatcher, nightly aggregates
Documentation siteDroplet, rsync on push. Unrelated to the application deploy.
Do not use Lovable's Publish button once Vercel is connected

Lovable can publish a project itself, and Vercel auto-deploys on every commit to main. Both work. Running both gives production two deployment paths, one of which bypasses the pull request, the QA gate and the promotion path entirely — and the two will disagree the first time someone is in a hurry.

Vercel is the deploy path. Lovable is the build environment. The Publish button is off the table from the moment the repository is connected.

Why Vercel rather than the droplet

Chosen for development speed, branch previews and a smaller devops surface. Self-hosting on the existing droplet is cheaper in cash and considerably more expensive in attention — backups, upgrades and TLS become someone's job, and there is no someone. The droplet keeps the documentation site and nothing else.

09 · Email

One provider, two send paths, one sending domain

Vercel has no email service. It is not a setting left unconfigured — the product does not exist. Vercel runs the functions; the sender is brought separately. That made this a decision rather than a default, and it is now taken: Resend.

Why Resend and not the Google Workspace account we already pay for

Google positions SMTP relay for on-premise devices and Gmail SMTP for personal correspondence — application mail is explicitly not the use case. Past the 10,000-per-day and 100-recipients-per-transaction caps, it cannot do what the table above commits to: no suppression list, no unsubscribe handling, no bounce or complaint webhooks, no per-message delivery log. All of that would be built by hand against a service that does not report failures back.

The sharper reason is reputation. Application mail sent through Workspace shares domain reputation with the team's real correspondence. A run of bounced quote emails degrades deliverability for the humans, and there is account-suspension risk on top.

Why Resend specifically, for this stack

First-party on all three sidesthe deciding factor

Lovable ships a Resend integration, Resend ships a Lovable one, and Supabase lists it as a recommended provider. No other provider in the category has support from every tool in this chain.

Templates live in the repoReact Email

Templates are React components rather than markup in a vendor's editor. They sit under version control with everything else, and they can use the design system's semantic tokens instead of a second, drifting set of styles.

Covers both send pathsSMTP + API

SMTP credentials for Supabase Auth, an HTTP API for the queue worker. One account, one domain reputation, one place to look when something did not arrive.

Phase 1 costs nothing3,000/mo · 100/day

Invitations plus approval requests is dozens of messages a month. The binding number is the 100-a-day cap, not the monthly one — see the batch warning below. Paid is $20/month for 50,000.

Where Postmark would have won, precisely

Postmark enforces message-stream separation and vets every sender, so its shared IP pools carry transactional traffic only. Resend does not enforce that separation by default and also sells broadcast, so its shared pool can carry campaign traffic from other tenants.

But notice what that difference is about. It bites when you send marketing alongside transactional and the two compete for one reputation. ConnectIQ sends no marketing email — it is out of scope in both phases — and a quote emailed to a named contact who is expecting it is transactional by any definition. Postmark's principal advantage solves a problem this system does not have. The residual is other tenants on the shared pool: real, small, and not measurable in advance.

The genuine triggers to revisit are narrower than "we email customers": if ConnectIQ ever sends campaign email, if quote deliverability becomes a measured problem rather than a feared one, or if volume grows past a few thousand a month.

The two paths

These are genuinely separate systems and both have to be configured, which is easy to miss because only one of them is visible in the codebase.

Supabase AuthApplication email
SendsInvitations, password resets, email confirmationQuote approval requests, and the signed approve/reject links
Configured inSupabase custom SMTP settingsplat_email_queue, drained by the scheduled worker
TransportResend SMTP credentialsResend HTTP API — no long-lived SMTP connection from a serverless function
TemplatesSupabase email templatesReact Email components in the repo
Built inM1M1 infrastructure, first real send in M5
Custom SMTP is not optional, and the default will stop M1 dead

Supabase's built-in sender is capped at two messages per hour, project-wide, and is explicitly not for production use. M1's exit criterion is "an admin can invite a colleague, approve them, and see the empty app" — on the built-in sender that fails while approving the third person. Configure Resend SMTP in Supabase before the access gate is tested, not after.

Even on custom SMTP, auth email defaults to 30 new users per hour. Fine here, but it is a setting, not a law — know where it is before someone concludes invitations are broken.

The sending domain

The application runs at connectiq.giant-pumpkin.com, and that is also the sending domain. This is worth stating as a deliberate choice rather than a convenience.

The domain choice already buys the isolation

SPF and DKIM are per-domain. Because connectiq.giant-pumpkin.com is a subdomain, Resend's records sit there and the Google Workspace records on giant-pumpkin.com are never touched. Root DMARC still passes on relaxed alignment. Application mail cannot damage the deliverability of the team's real correspondence, because it is not sharing its authentication.

RecordWherePurpose
DKIMconnectiq.giant-pumpkin.comSigns outbound mail so recipients can verify it was authorised.
SPF (TXT)send.connectiq.giant-pumpkin.comLists the addresses permitted to send. Resend's default is a send. subdomain.
MXsend.connectiq.giant-pumpkin.comReturn path. This is how bounce and complaint feedback reaches us at all — without it the suppression list never fills.
DMARC (TXT)_dmarc.connectiq.giant-pumpkin.comPolicy for what a recipient does when authentication fails. Start at p=none and tighten once real traffic is observed.

From address: notifications@connectiq.giant-pumpkin.com. Verification usually completes within ten minutes; Resend keeps re-checking for 72 hours before marking the domain failed.

Domain verification is a day-one M1 task, and it blocks the exit criterion

Until the domain is verified, Resend will only deliver to your own address. "An admin can invite a colleague, approve them" cannot be demonstrated before then. It is a DNS change with propagation time rather than a code change, so it wants starting on the first day of M1 — not on the afternoon the invitation flow is finished.

Secrets, and the one rule that is specific to Lovable

The API key goes in Supabase secrets. Never in Lovable, never in the repo.

Lovable's own guidance is that it does not securely handle API keys on its own, and that secrets should be stored through the Supabase integration. That happens to agree with the rule already in force: integration credentials live in the platform secret store, rotated on staff change, never committed.

Bounces, complaints and the suppression list

plat_suppressed_emails is not decoration. Resend posts bounce and complaint events to a webhook, and that endpoint writes the suppression rows — which is the only thing that stops the system re-sending to an address that has already hard-bounced and taking the domain reputation down with it.

That webhook is an unauthenticated public endpoint, so the standing rule applies without exception: verify the signature before any write. It also belongs in int_webhook_events — inbound-external — and never in plat_domain_events, which is internal.

The binding limit is 100 a day, not 3,000 a month

The free tier caps hard at 100 messages per day — it does not spill into an overage charge, it stops. Ordinary use never approaches it, but one flow can: the renewal step generates a quote per company from a selected set of subscriptions, so acting on thirty accounts at once and emailing each spends a third of the day's allowance in a single click.

Two consequences. The queue worker must handle a provider rejection as a retryable state, not a dropped message — that is what plat_email_queue is for, and a daily cap is exactly the failure it should absorb invisibly. And if this becomes routine, the $20 tier removes the cap; it is not a reason to reconsider the provider.

What phase 1 sends, and what it does not

Two sends: user invitations and quote approval requests. Both go to staff. Customer-facing sending — the quote itself — is governed by DR-40, because the delivery plan currently says three things that cannot all be true at once.

Either way the provider is settled. Resend delivers to external recipients on any tier once the domain is verified; DR-40 changes the operational work around sending, not who sends.

10 · Guardrails

The structure exists before Lovable is asked to build into it

This is the discipline the whole model rests on. A code-generating assistant will invent a structure if it is not given one, and two assistants inventing two structures in one repository is the failure this is designed to prevent.

The rule

Do not let each branch invent its own structure. The structure must already exist before Lovable starts building.

So before asking Lovable to build the quotation surface, the empty structure is created first:

src/domains/quotation/

  • components/
  • pages/
  • services/
  • types/
  • hooks/
  • constants/

Then the prompt says

  • Work only inside src/domains/quotation.
  • Do not modify global routing, layout, auth, the Supabase client, shared components, package files or database migrations unless you first explain why.
  • Before coding, list the files you intend to edit.

The knowledge file

The knowledge file is the project's brain. It is sent with every prompt, defines the context and the guardrails, and is what keeps Lovable working inside its own domain rather than editing centralised files in a repository it shares with someone else.

It can be generated from Plan mode with a T=0 prompt — "generate knowledge for my project at T=0 based on the features I've already implemented" — which resets the knowledge base to a new baseline by having the assistant analyse and document what currently exists. It freezes progress at a known state so development continues from there rather than from a stale description.

Four places now describe the same guardrails — assign one owner each

The architecture's appendix holds the binding rules — prefixes, cross-domain access, identifiers, the outbox. The delivery plan's 00-standing-context.md holds the paste block a session with no Knowledge panel receives. This page holds how the files are generated and kept current. And style-kit/LOVABLE_WORKSPACE_KNOWLEDGE.md and style-kit/LOVABLE_PROJECT_KNOWLEDGE.md hold the Lovable projection — the same rules in imperative form, split across Lovable's two Knowledge slots so a Lovable session carries them without a paste. Lovable caps each slot at 10,000 characters, which is why the projection is two files and not one, and why it is a compression rather than a copy. They must not drift: when a rule changes, it changes in the architecture first and the others are regenerated from it. A guardrail that says two different things in two files is worse than one that says nothing.

Status and workflow constants

The system depends heavily on workflow states, and every one of them is a place where four spellings of the same idea can appear. Create shared constants and enums early — before the first status field is written, not after the fourth.

Bad

  • "Completed"
  • "complete"
  • "Done"
  • "Job Complete"

Better

  • JOB_STATUS.COMPLETED
  • QUOTE_STATUS.APPROVED
  • READINESS_STATUS.READY_FOR_SCHEDULING

This matters for automation later, and it is already binding: the architecture's rule set requires shared status constants and forbids writing or comparing a raw status string. It also has a specific bite in this system — quote status and quote approval state are two separate axes, and a loose string makes them trivially easy to conflate.

11 · Shared files

One repository, so the risk is collision rather than drift

The source workflow warns about package drift — one developer upgrading while another does not, producing a merge that type-checks in both branches and fails in neither until it lands. That failure needs independent repositories, and there is only one here. One package.json, one lockfile, one set of generated types. Drift cannot happen.

What replaces it is narrower and more common: two branches editing the same shared file. The domain folders keep feature work apart, but a handful of files sit outside every domain and belong to all of them.

Files outside every domain

  • package.json, package-lock.json / pnpm-lock.yaml
  • Global routing and layout
  • The Supabase client and generated types
  • Auth, the role check, shared components
  • Migrations — numbered, so two branches adding one collide by construction
  • Shared status constants and enums

How they stay uncontested

  • They are on the guardrail list — Lovable does not touch them without explaining why first.
  • A dependency change is its own commit, reviewed as shared infrastructure rather than as part of a feature.
  • The lockfile is committed and never regenerated as a side effect.
  • Generated Supabase types come from one command in CI, not from whoever ran it last.
  • Migration numbering is claimed at the point the branch is cut, not at the point it merges.
The conflict is the signal, not the problem

A merge conflict on a domain file means the branch got too big — one feature per branch, merge, go again. A merge conflict on a shared file means something reached outside its domain, which is a guardrail failure and worth understanding rather than resolving. Fix the second kind by asking why it happened.

12 · Quality bar

What "done" means for a milestone

Moved here from the delivery plan, because it is the same bar for every milestone. The plan says what each one delivers; this says what any of them has to clear.

Every milestone

  • Types strict, no escape hatches left in.
  • Every new table has RLS policies in the same migration.
  • Permissions verified from the database side, not just by hiding buttons.
  • UI uses semantic tokens only — the lint rule passes.
  • No cross-domain import — that lint rule passes too.
  • Keyboard reachable, visible focus, status never conveyed by colour alone.
  • Empty, loading and error states designed, not left to default.
  • Generated browser tests pass for the epic.
  • Demoed to a real user of that surface before it is called done.

Tested specifically — see §07

  • Money maths, with real coverage.
  • The per-location multiplication, against ten historical deals.
  • Permission resolution, as a matrix per role.
  • Import mapping, against malformed fixtures.
  • Sync and event idempotency.
  • Quote approval history staying attached to what was approved.
A milestone is not done because the code runs

The bar that catches the most is the last one on the left: demoed to someone who will actually use that surface. Every other item can be satisfied by a system that is technically correct and practically wrong.

13 · Decisions

Four, three of which change what the delivery plan said

Each one overturns something the plan had settled. They are recorded here, and the plan is annotated rather than quietly edited — the record of what was decided, and why it changed, is the point.

ADR-07 Supersedes ADR-06

Three tiers, including a persistent staging environment

Decision — development (a preview branch per feature), staging (persistent, viewable), production (main, Vercel). No fourth environment: the migration rehearses in production before anyone uses it — see §03.

Beat — development and production only, which is what ADR-06 chose.

Because — ADR-06's reasoning was that "a staging environment that nobody keeps populated becomes a source of false confidence and a second thing to maintain". Two things changed. Supabase branching makes the environment cheap and reproducible rather than a hand-tended server, and a seed file makes "nobody keeps it populated" a solved problem rather than a habit. More importantly, a code-generating assistant produces whole features at once, and there has to be somewhere those can be seen and exercised before they reach production.

Cost — roughly ten dollars a month for the always-on staging branch, plus the discipline of deleting preview branches. And ADR-06's warning has not stopped being true: if staging is allowed to go stale, it becomes exactly the false confidence it warned about.

ADR-08 Closes DR-28

Vercel for the application, managed Supabase for the database

Decision — the production branch connects to Vercel and nothing else does. The database is managed Supabase on the Pro plan. The droplet keeps the documentation site.

Beat — self-hosting the application and database on the existing droplet.

Because — development speed, branch previews, and a devops surface small enough for two people. Self-hosting is cheaper in cash and considerably more expensive in attention. Pro is not a preference: branching for preview environments requires it, so the environment model in ADR-07 depends on this decision.

Cost — a platform bill, and vendor coupling to two managed services. Also the serverless runtime constraint survives: no native binaries, no child processes, no long-lived connections, which still limits what the quote print view and the import module can use.

ADR-09 Revises the plan's testing stance

Generated browser tests per epic, hand-written tests for the numbers

Decision — Lovable's browser, frontend and backend testing run across every epic. Money maths, the per-location multiplication, permission resolution, import mapping and idempotency are written and owned by us.

Beat — no broad end-to-end suite at all, which is what the delivery plan chose.

Because — the original reasoning was that the maintenance cost of a broad suite outruns its value at this team size. That is a statement about hand-written tests. When the suite is generated and regenerated by the same tool that writes the feature, the maintenance cost is no longer the developer's, and the trade flips.

Cost — generated tests can be confidently wrong, and a suite nobody wrote is a suite nobody fully understands. Hence the division in §07: generated tests prove the application still works, hand-written tests prove the numbers are right, and only the second kind blocks a merge on its own.

ADR-10 Closes the email row

Resend for both send paths, from connectiq.giant-pumpkin.com

Decision — Resend. SMTP credentials configured as Supabase custom SMTP for auth email; the HTTP API used by the queue worker for application email. Sending domain connectiq.giant-pumpkin.com, from notifications@. API key in Supabase secrets.

Beat — the Google Workspace account we already pay for, and Postmark.

Because — Vercel has no email service, so a provider was required either way. Workspace is positioned for personal correspondence and on-premise relay, cannot supply a suppression list, unsubscribe handling or bounce webhooks, and would put application mail on the same domain reputation as the team's real email. Against Postmark the deciding factor is first-party integration on all three sides of this stack — Lovable, Supabase and Resend each document the others — plus React Email templates that live in the repo under the same tokens as the rest of the UI.

Cost — nothing in phase 1. A second vendor in the chain, a deliverability reputation that has to be built from zero on a new subdomain, and a hard cap of 100 messages a day on the free tier that a batch operation could reach in one action. The paid tier removes the cap at $20/month.

What this does not change

ADR-01 through ADR-05 stand. One deployable separated by domain prefix; Postgres with RLS as the permission model; read-only integration in phase 1; origin identity in one mapping table; schema changes as numbered migrations in the repository. ADR-05 is worth restating in this context: Lovable and Supabase branching will generate migrations, and those migrations land as committed files. The console is still never where a change is made.

14 · Open questions

One open, two closed

Numbered in the delivery plan's register series so there is one sequence across both documents. One remains: the reach of email. The two closed entries are kept because the policies they settle are referenced from elsewhere on the page.

Development strategy

DR-38 · Who owns the seed file, and how complete does it have to be? Closed

Decided: two artifacts, not one. seed.sql is synthetic, committed to the repo, created in M1 and grown by whoever builds each milestone — it must always carry enough to open the screens that milestone builds. Staging's data is a separate thing entirely: a copy of production, unmodified, set once and then allowed to drift. It is not scrubbed, because everyone with access to staging already reads the same records in HubSpot and Airtable — see §06 for the expiry condition on that.

Full policy and the reasoning in §06. One consequence to plan for: production does not exist until M7, so staging is populated from a scrubbed HubSpot export during the build — the same export the import work needs anyway.

Closed · see §06

DR-39 · What is the migration rehearsal environment? Closed

Decided: there isn't one. The migration rehearses in production. Until the parallel run begins, production is empty and unused — the most accurate possible target, at no extra cost, with the real permission model and RLS policies that a copy only approximates.

It holds on one line. Wipe-and-retry is allowed until the point of no return — the first real user action, not a date. After that production can never be wiped again. Back up before every attempt, disable outbound email during runs, and keep the original gate: two clean runs and a rep confirming their own accounts. Full conditions in §03.

The parallel run then is the probation period, with HubSpot still readable as the fallback.

Closed · see §03

DR-40 · Does the system email customers, or only staff?

The delivery plan currently says three things that cannot all be true. Its scope section lists "transactional email — quote approval requests and user invitations only", both internal. Its deferred list defers "send-from-CRM". But M5's third wizard step composes a message to the customer and enforces that the recipient contact has an email address, with a fallback to download the PDF and mark it sent by hand.

Assumed: staff only in phase 1 — the quote step produces a compose link and an outbound record, and the human sends it from their own mailbox.

This does not change the provider. Resend sends to external recipients on any tier once the domain is verified, and a quote to a named contact expecting it is transactional. What changes is the work around it:

  • Bounce and complaint handling stops being hygiene. The suppression webhook becomes load-bearing — external recipients hard-bounce and mark as spam in ways staff never will.
  • DMARC should tighten from p=none to quarantine or reject, once a few weeks of real traffic have been observed.
  • The 100-a-day free cap starts mattering. The renewal flow generates one quote per company from a selected set of subscriptions — batch that across thirty accounts and email each, and a third of the daily allowance goes in a single action.
  • Complaint rate becomes a number someone looks at rather than a field nobody reads.

Needs · Sky + sales · before M5 builds step three

One sequencing consequence, already certain

Lovable cannot import an existing repository. The delivery plan's M1 opens with "repository, CI, and the three lint rules" — and the repository cannot be created first and then attached, because the flow only runs Lovable → Git. M1 has to start from the Lovable project, with CI, lint rules, the RLS check and the migration dry-run added to the repo it produces. This is not an open question; it is a constraint, and M1's prompt needs to reflect it.

Sources

Where this came from

Lovable — knowledge filesdocs

The knowledge file, what it is sent with, and generating one from Plan mode.

docs.lovable.dev/features/knowledge

Supabase — branchingdocs

Preview and persistent branches, the Pro-plan requirement, seeding, and why no production data is copied.

supabase.com/docs/guides/deployment/branching

Supabase — branching usagepricing

US$0.01344 per hour on Micro compute, no fixed fee per branch, and the fact that compute credits do not offset branching.

supabase.com/docs/guides/platform/manage-your-usage/branching

Lovable — connect to Resenddocs

The integration, and the rule that the API key is stored through Supabase rather than in Lovable.

docs.lovable.dev/integrations/resend

Supabase — custom SMTPdocs

Why the built-in sender is capped at two an hour and cannot be used in production.

supabase.com/docs/guides/auth/auth-smtp

Resend — managing domainsdocs

The MX, SPF, DKIM and DMARC records, the send. subdomain pattern, and the 72-hour verification window.

resend.com/docs/dashboard/domains

Google Workspace — SMTP relaydocs

The limits and intended use case that ruled it out for application mail.

knowledge.workspace.google.com — SMTP relay

Staging & production environments in Lovablewalkthrough

The promotion path in Fig 1, end to end: branching from staging, the pull request into staging, the human QA pass, the release preview on the staging-to-main PR, and Vercel auto-deploying from main. Also the source for the working model in §04 — several developers at once, feature by feature, none touching another's code.

youtube.com/watch?v=7m-gkwQwbKU

Team-based development in Lovablearticle

The project-per-domain isolation pattern this page is built on.

medium.com — Gabriel Morais

Pricing and limits verified July 2026. Usage-based figures move; re-check before committing a budget.