Development strategy · the machinery
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.
01 · The split
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.
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
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.
| Layer | Decision | Confidence | Note |
|---|---|---|---|
| Database | Postgres (Supabase) with row-level security on every table | Settled | Multi-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. |
| Auth | Supabase Auth — email + password, Google OAuth, no anonymous sign-in | Settled | Roles live in plat_user_roles and are checked by a SECURITY DEFINER function. Never on the profile row. |
| Backend | Server functions in the app, not a separate service | Settled | One deployable, internally separated by domain prefix — the separation rules are in the architecture. |
| Cross-domain effects | Outbox — plat_domain_events, written in the same transaction as its cause, drained by a scheduled dispatcher | Settled | Binding in the architecture. M1 work, not a retrofit. |
| Frontend | React + TypeScript strict, file-based routing, query cache with loader-fetched routes | Settled | Forms on a schema validator shared with the server. Charts from one library, icons from one family. |
| Styling | Semantic tokens from style-kit/tokens.json. No raw palette classes in feature code | Settled | Enforced by lint rule, not by review discipline. |
| Code generation | Lovable, one project per domain, working inside a fixed folder structure | Settled | See §04 and §09. The structure exists before Lovable is asked to build into it. |
| Hosting — app | Vercel, connected to the production branch only, serving connectiq.giant-pumpkin.com | Settled | Closes the plan's open hosting question. Chosen for dev speed, staging previews and reduced devops surface. See §08. |
| Hosting — database | Managed Supabase, Pro plan | Settled | Pro is not optional — branching for preview environments requires it. See §06. |
| Background work | Queue tables in Postgres, drained by a scheduled worker | Settled | Email sending, Airtable sync, the event dispatcher and nightly aggregates all use the same pattern. No second infrastructure component. |
| Resend — transactional only, templated with React Email, suppression list fed by webhook | Settled | First-party integration on all three sides of the stack. Covers both send paths from one account. See §09. | |
| Docs site | Static HTML on the existing droplet, deployed from this repo | Settled | What you are reading. No build step, no framework, no dependency to keep current. |
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
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.
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.
| Tier | Branch | Database | Who sees it | Lifetime |
|---|---|---|---|---|
| Development | Feature branch, per domain | Supabase preview branch — starts empty, seeded from seed.sql | The developer, and Lovable | Deleted when the work merges. Unused branches cost money. |
| Staging | staging — persistent | Persistent branch. A copy of production, unmodified — see §06 | Anyone, including reps for early feedback. Viewable from Lovable. | Standing. |
| Production | main — persistent | Production project, real data, nightly backups | Everyone | Standing. |
| Production, pre-launch | main | The real thing, empty — then loaded, wiped and reloaded per import attempt | Whoever is running the import | A phase, not an environment. Ends the moment a real user touches it. |
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:
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.
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
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.
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.
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.
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.
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.
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 boundary — src/domains/<domain>/ — and the prompt discipline in §10, not a separate workspace.
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
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.
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.
main deploys. Rollback is a redeploy of the previous commit.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 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.
staging and production. Always on, seeded, and paid for continuously.
Feature work. Created with the branch, deleted with it. Usage-billed by the hour.
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.
One feature branch alive for 30 hours.
One always-on staging branch, 720 hours.
Branching for preview environments needs it. GitHub integration and CLI deployment work on any plan.
Compute credits do not apply to branching. The monthly credit that offsets your main project does not offset branches.
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.
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.
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.
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.
| Tier | Data | Where it comes from | Refresh |
|---|---|---|---|
| Feature branch | Dummy. Enough to exercise the domain being built, nothing more. | seed.sql, committed to the repo | On branch creation. Reseeding means recreating the branch. |
| Staging | The same data as production. Not scrubbed, not subsetted. | A restore from a production backup | Set once, then allowed to drift. Close enough for QA, not a mirror. |
| Production | Real. | The migration, then the business | Live. Nightly backups. |
| Production, pre-launch | Real. Duplicate companies, malformed emails, orphan contacts — the defects are what is being tested. | A dated HubSpot export | Wiped and reloaded per attempt, until the point of no return. |
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.
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.
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
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 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
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.
| Concern | Where it lives |
|---|---|
| Production domain | connectiq.giant-pumpkin.com — a subdomain, which is what keeps application email off the Workspace root. See §09. |
| Build and deploy | Vercel, triggered by merge to main |
| Preview deploys | Vercel, per branch — how staging is looked at without a second host |
| The release preview | Opening 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 secrets | Vercel project settings and the Supabase secret store. Never the repo. |
| Rollback | Redeploy the previous commit |
| Database migrations | Numbered SQL files in the repo, applied through the Supabase GitHub integration |
| Scheduled work | Queue tables drained by a scheduled worker — email, sync, the event dispatcher, nightly aggregates |
| Documentation site | Droplet, rsync on push. Unrelated to the application deploy. |
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.
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
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.
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.
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 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.
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.
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.
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.
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 Auth | Application email | |
|---|---|---|
| Sends | Invitations, password resets, email confirmation | Quote approval requests, and the signed approve/reject links |
| Configured in | Supabase custom SMTP settings | plat_email_queue, drained by the scheduled worker |
| Transport | Resend SMTP credentials | Resend HTTP API — no long-lived SMTP connection from a serverless function |
| Templates | Supabase email templates | React Email components in the repo |
| Built in | M1 | M1 infrastructure, first real send in M5 |
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 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.
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.
| Record | Where | Purpose |
|---|---|---|
| DKIM | connectiq.giant-pumpkin.com | Signs outbound mail so recipients can verify it was authorised. |
| SPF (TXT) | send.connectiq.giant-pumpkin.com | Lists the addresses permitted to send. Resend's default is a send. subdomain. |
| MX | send.connectiq.giant-pumpkin.com | Return 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.com | Policy 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.
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.
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.
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 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.
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
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.
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/src/domains/quotation.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.
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.
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.
"Completed""complete""Done""Job Complete"JOB_STATUS.COMPLETEDQUOTE_STATUS.APPROVEDREADINESS_STATUS.READY_FOR_SCHEDULINGThis 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
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.
package.json, package-lock.json / pnpm-lock.yamlA 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
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.
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
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.
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.
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.
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.
connectiq.giant-pumpkin.comDecision — 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.
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
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.
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
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
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:
p=none to quarantine or reject, once a few weeks of real traffic have been observed.Needs · Sky + sales · before M5 builds step three
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
The knowledge file, what it is sent with, and generating one from Plan mode.
docs.lovable.dev/features/knowledgePreview and persistent branches, the Pro-plan requirement, seeding, and why no production data is copied.
supabase.com/docs/guides/deployment/branchingUS$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/branchingThe integration, and the rule that the API key is stored through Supabase rather than in Lovable.
docs.lovable.dev/integrations/resendWhy the built-in sender is capped at two an hour and cannot be used in production.
supabase.com/docs/guides/auth/auth-smtpThe MX, SPF, DKIM and DMARC records, the send. subdomain pattern, and the 72-hour verification window.
The limits and intended use case that ruled it out for application mail.
knowledge.workspace.google.com — SMTP relayThe 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-gkwQwbKUThe project-per-domain isolation pattern this page is built on.
medium.com — Gabriel MoraisPricing and limits verified July 2026. Usage-based figures move; re-check before committing a budget.