Every product with customers needs the same three things before it can do anything distinctive: a way for people to sign in, a way to take their money, and a path from signing up to actually using it.
None of them differentiate you. All of them have to work perfectly, because a broken password reset or a failed payment is not a feature gap — it is the product not functioning. And on a first build they routinely consume a third of the calendar.
This is what each one actually involves, what to buy versus build, and why they are the reason starting from a settled template shortens a project more than any framework choice.
Why do these three consume so much time?#
Because the happy path is a fraction of the work and the surrounding cases are where the hours go.
Signing up is one form. What surrounds it: email verification, password reset, expired reset links, a user who signs up twice, someone who registers with a social provider and later tries a password, session expiry, logging out everywhere, and what happens when an email address changes.
Nobody scopes those. They scope "login", which sounds like an afternoon, and then discovers the states are the product. Billing is the same shape and worse, because the failure modes involve money.
The reason this matters for planning is that it is genuinely predictable work. It is the same every time, which makes it exactly the thing to solve once and reuse rather than re-derive per project.
What does authentication actually have to handle?#
More than login, and the list is stable across products.
| Flow | Frequently forgotten | Cost if wrong |
|---|---|---|
| Sign up and verify email | Verification expiry | Unusable accounts |
| Sign in | Rate limiting | Credential stuffing |
| Password reset | Token single-use and expiry | Account takeover |
| Session management | Revocation on password change | Stale access |
| Social or magic-link login | Linking to an existing account | Duplicate users |
| Change email address | Verifying the new one first | Lockout or takeover |
The right-hand column is why I do not hand-roll this. Most of these failures are security failures rather than bugs, and the cost of getting one subtly wrong is not proportional to the effort saved by writing it yourself.
The one part worth writing yourself is the mapping between the identity provider and your own user record — a users table keyed to the provider id, so your data model is not scattered with the provider's identifiers. That keeps a future migration to one table rather than to every table.
Should you build authentication?#
Almost never. Buy it, and spend the time on the part that is actually yours.
The argument for building is usually cost or control. Cost is generally not real — the free tiers on managed providers cover more users than most products reach, and the paid tiers arrive around the point where revenue exists. Control is real and is mostly satisfied by keeping your own user table.
The situation where building is right: an unusual requirement that no provider handles, or a hard constraint about where credentials may be stored. Both are rare, and both are worth confirming rather than assuming.
The org model test decides which to buy. Individual users are well served by the auth bundled with your database platform; organisations with roles and invitations are worth a dedicated identity product, because that is weeks of work rather than days.
What does billing actually involve?#
Substantially more than a checkout button, and the parts after payment are where the complexity lives.
Taking the first payment#
The easiest part, and largely solved by a hosted checkout. Redirect, take the payment, return. Building a custom payment form means handling card data, which changes your compliance obligations considerably for no benefit.
Knowing what somebody is entitled to#
The part that is genuinely yours. A subscription in the payment provider is not access control — your application needs to know a customer is on a plan and what that permits, and it needs to be right when the webhook is late.
Changes mid-cycle#
Upgrades, downgrades, cancellations, and what happens to the remainder of a period already paid for. Proration is a decision, and deciding it deliberately once beats discovering an edge case per customer.
Failed payments#
Cards expire routinely, and a failed renewal is a customer you keep if it is handled well and lose if it is not. Retry schedules, a notification that does not read as an accusation, and a grace period before access is removed.
The distinction that keeps this manageable is that the payment provider owns money and your application owns entitlement. They synchronise through webhooks, and treating the provider as the source of truth for both is what produces access that is briefly wrong after every change.
Trials sit awkwardly across that boundary and are worth deciding early. A trial managed by the payment provider requires a card up front, which suppresses signups and produces cleaner conversion. A trial managed in your own database needs no card, converts a larger number of signups into users and a smaller proportion into customers. Both are defensible; running one while the code assumes the other is what produces accounts in an undefined state.
Whichever you choose, the trial should be a field on your subscription record rather than an inference from an absent one. "No subscription row means trialling" works until somebody cancels, and then a cancelled customer looks identical to a new one.
How do you keep entitlement correct?#
Store it in your database, updated by webhooks, and never derive it from a live API call in a request.
model Subscription {
id String @id
orgId String @unique
plan String // your language, not the provider's
status String // active | past_due | canceled
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
@@index([status, currentPeriodEnd])
} Two properties matter. Reading entitlement is a local query, so a permission check never depends on a third-party API being reachable. And the plan is expressed in your own vocabulary rather than the provider's price identifiers, so changing pricing is a mapping change rather than a migration.
Webhooks arrive out of order, more than once, and occasionally late. Handle them idempotently, keyed on the event id, and reconcile periodically against the provider for anything that drifted — a nightly job comparing the two is unglamorous and catches the cases webhooks missed.
What makes onboarding worth building?#
It is the one of the three that is genuinely product-specific, and the only one where effort differentiates you.
The purpose is reducing time to value — the gap between signing up and the product having done something useful. That gap is where most trials are lost, and it is not solved by a tour.
Product tours are worth a word, because they are the reflexive answer and they usually address the wrong problem. A tour explains an interface; it does not make the product useful. If somebody needs six tooltips to reach the first valuable action, the sequence is too long, and shortening it beats explaining it. Tours are a reasonable addition once the path is short and genuinely non-obvious, and a poor substitute for making the path short.
The measurement that tells you whether any of this is working is not completion of the onboarding flow. It is how many accounts reach the first genuinely useful action, and how long that takes. Those two numbers point directly at what to change, whereas a completion percentage mostly tells you how patient people were.
The empty state is the onboarding#
A new account has no data, and the default is a blank screen with a table header. What it should have is the shortest path to one useful thing: sample data to explore, a single obvious action, or an import that populates it.
Ask for the minimum#
Every field between signing up and the first useful moment costs completion. Company size, role and how they heard about you can be asked later, after the product has demonstrated something.
Progressive rather than upfront#
Configuration requested when it is needed makes sense; the same configuration requested as a nine-step wizard before anything works does not. Defer everything that is not required for the first action.
What should you buy and what should you build?#
| System | Build | Buy | My default |
|---|---|---|---|
| Authentication | Rarely | Usually | Buy |
| Payment processing | Never | Always | Stripe |
| Entitlement model | Always | Never | Build |
| Transactional email | Never | Always | Resend |
| Onboarding | Always | Never | Build |
The pattern is that anything security-critical or compliance-heavy is bought, and anything encoding what your product means is built. Entitlement is on the build side precisely because it is where your plans, limits and permissions live.
Transactional email deserves its place. Running a mail server is a job, and deliverability is a discipline — SPF, DKIM, DMARC, warming, bounce handling. A provider does this and it costs nothing at the volumes an early product sends.
How do you model plans and limits?#
In your own vocabulary, in one place, and expressed as data rather than as conditionals scattered through the codebase.
One definition of what each plan permits#
A single object mapping plan names to limits — seats, projects, storage, monthly AI spend. Every check reads from it, so changing a limit is one edit and adding a plan does not require finding every place a comparison was written.
export const PLANS = {
free: { seats: 1, projects: 3, aiCents: 200 },
team: { seats: 10, projects: 50, aiCents: 5_000 },
scale: { seats: 50, projects: 500, aiCents: 25_000 },
} as const;
export function limitFor(plan: PlanName, key: LimitKey) {
return PLANS[plan][key];
} Check on the action, not on the page#
The enforcement point is the moment something is created, not the moment a screen is rendered. Rendering checks are for hiding buttons; they are not access control, and a client that can call the endpoint directly is a client that will.
Decide what happens at the limit#
Blocking is the obvious answer and rarely the best one. Allowing the action and prompting to upgrade converts better for soft limits like projects, while hard limits — seats, spend — genuinely need to block. Decide per limit rather than applying one policy to all.
Handle downgrades gracefully#
A customer on fifty projects who downgrades to a plan allowing three is the case nobody scopes. Deleting their data is unacceptable; silently keeping the excess undermines the plan. Read-only above the limit, with a clear prompt, is usually the right compromise.
What about the admin side?#
The part nobody scopes and everybody needs by week two, because supporting customers without it means running SQL against production.
The minimum is the ability to find a customer, see their plan and subscription status, and change it. That covers the overwhelming majority of support requests — a refund, a manual upgrade, extending a trial for somebody evaluating.
It does not need to be a dashboard. On an early product, a handful of carefully written scripts with confirmation prompts is faster to build and safer than a UI, and the database itself covers reporting. Building an admin panel is one of the first things I cut from an MVP scope, and one of the first things added afterwards.
What is worth building early is impersonation — signing in as a customer to see what they see. It turns most support conversations from a diagnostic exchange into a look, and it needs to be audited, restricted to staff, and obviously indicated in the interface so nobody forgets which account they are in.
How long does this take on a real project?#
Around five days on a standard web app, and that is with a settled stack and prior implementations to draw on.
On a 19-day MVP it was days four through eight: authentication and the account model, then billing and the shell. Roughly a quarter of the calendar, spent on nothing a user would describe as a feature.
That is the honest number, and it is why templates matter more here than anywhere else. The work is genuinely identical between projects, so the second time it is three days and the fifth time it is two — which is compounding you cannot get by choosing a better framework.
These three are the least interesting and least optional part of any product. Solving them once is the difference between a five-week build and an eight-week one.
What does testing these look like?#
Different from testing features, because the failure modes are states rather than logic, and the expensive ones only occur in sequences.
The tests worth writing by hand are the sequences: sign up, verify, log in, subscribe, upgrade, fail a renewal, recover. Each step individually is trivial and the interesting bugs live in the transitions — an upgrade during a grace period, a password reset for an account that never verified.
Webhooks need explicit tests for the awkward cases rather than the happy one. Deliver the same event twice and assert nothing changed the second time. Deliver a cancellation before the subscription creation it refers to, which happens more often than seems reasonable. Deliver an event for a customer that does not exist locally.
And test the entitlement check on the server directly, not through the interface. A test driving the UI proves the button is hidden; a test calling the endpoint proves the action is refused. Only the second is a security test, and it is the one that matters when somebody has read your JavaScript.
What gets these wrong most often?#
Four failures I see repeatedly, all cheap to avoid and expensive to discover.
Entitlement checked in the client#
Hiding a feature from a free plan in the interface, without the server enforcing it. The check has to be on the server, at the point the action is performed, because the interface is a suggestion.
No grace period on failed payment#
Access removed the moment a renewal fails means a customer whose card expired is locked out before they know. A few days of grace with a clear notification keeps most of them.
Deleting accounts that owe money or hold data#
Account deletion interacting with active subscriptions, retained invoices and other users' shared records. Decide what deletion means before offering it, because the alternative is deciding it during a support conversation.
Email that quietly does not arrive#
Password reset and verification are the two emails a product cannot afford to lose, and they fail silently. Domain authentication set up before launch, and a deliberate test that one actually arrives, is the whole fix.
Conclusion#
Buy authentication and payment processing, build entitlement and onboarding. Keep your own user table keyed to the identity provider, and your own subscription record updated by idempotent webhooks so a permission check is a local query rather than an API call.
Treat the states around each flow as the actual scope. Password reset expiry, out-of-order webhooks, failed renewals, the empty state on day one — those are the work, and scoping "login and payments" as two features is how a fortnight becomes five weeks.
And solve it once. This is the most reusable code in any product because it is the least specific, which makes it the clearest argument for building from a template rather than from a blank repository each time.
There is a sequencing point worth making for anyone planning a first build. These three are not the thing to defer until the product works, even though they feel like plumbing. The data model they imply — users, organisations, subscriptions, entitlement — is the same data model everything else attaches to, and building features first means building them against a user concept that does not exist yet and retrofitting one later.
That is why five days near the start is the right shape rather than a week near the end. It is not that authentication is urgent; it is that the shape of your data is decided by it, and every feature built before that decision is a feature built on a guess.