Communications (post #6) is how the practice talks to patients. The money layer is how patients pay for the relationship. It touches every patient eventually, it's the part of the application where a bug costs real dollars rather than a re-render, and it's where I've spent more debugging time per line of code than anywhere except the video visit.
The framing is the medical ketamine practice in Florida and New Jersey, the same one the earlier posts describe. The money layer for a private-pay medical ketamine practice is its own shape: no insurance billing to integrate (which removes an enormous category of complexity), but a real product matrix of treatment courses and membership structures, recurring subscriptions that have to handle failed payments gracefully, refunds that have to be auditable, and the specific edge cases that turn out to eat the most time. This post is the architecture and the bugs.
Stripe, and only Stripe
The payments vendor is Stripe, and there was never a real alternative. Post #3 covered why. This post is about how it's wired.
The principle that matters most: the application never stores a card number, never sees a CVV, never touches raw payment credentials. Stripe Checkout and Stripe's hosted payment surfaces handle the card capture. The application stores a Stripe customer ID, a subscription ID, and the metadata that links those back to a patient record. When a patient pays, the money moves through Stripe and the application learns about it through webhooks. The application's job is to record what happened and react to it, not to process the payment itself.
This is not just a convenience. It's the single biggest reduction in compliance surface available to a solo healthcare practice. PCI-DSS scope is enormous if you handle card data and nearly nonexistent if you let Stripe handle it and only ever hold tokens. Combined with the HIPAA surface from everything else, the last thing a solo doctor wants is to also be in scope for card-data breaches. Let Stripe own the cards.
The product matrix
A private-pay medical ketamine practice sells a small but real matrix of products, and the axis that organizes it is treatment duration. In my application they're modeled as Stripe products with associated prices, and the application maps each one to a ProductType enum so the rest of the code can reason about them without hardcoding Stripe price IDs everywhere.
The matrix covers the initial evaluation (the eligibility-and-assessment first step before any treatment begins) and then the treatment courses themselves, priced by length: a one-month course, a three-month course, a six-month course, with the longer courses priced at a per-month discount that reflects the commitment. On top of the courses there's a maintenance tier for patients who have completed an initial course and continue on a lower-frequency ongoing basis, plus a handful of discrete items: documentation and letter fees for the kinds of requests covered in post #5, and a late-cancellation fee. Longer courses are the better clinical and financial fit for most patients, and the pricing is structured to make that the natural choice without pushing anyone into a commitment before the initial evaluation confirms it's appropriate.
The reason to model these as an enum mapped to Stripe rather than referencing Stripe price IDs directly throughout the code: Stripe price IDs are opaque strings that differ between test mode and live mode and change whenever you adjust pricing. If the code says if productType === ProductType.THREE_MONTH_COURSE, that survives a price change. If the code says if priceId === 'price_1ABC...', it breaks the next time you re-price, silently, in a way that's hard to catch in testing. The enum is the application's stable vocabulary; the Stripe price ID is a deployment detail that lives in one mapping table.
The matrix is small on purpose. Every product is something I have to be able to explain to a patient, refund correctly, report on for taxes, and reason about in the subscription lifecycle. Adding products multiplies the edge cases. I add a product when there's a clear patient-facing reason, not because Stripe makes it easy to create another price.
Two Stripe accounts
The practice runs two Stripe accounts. The separation is deliberate: different brands and revenue streams that I want kept financially distinct for clean accounting and clean tax reporting, rather than commingled in a single account with metadata tags trying to tell them apart after the fact.
The incomplete-onboarding cron
The single highest-value piece of revenue automation in the money layer is the incomplete-onboarding sequence, and it's worth describing in detail because the pattern generalizes.
A meaningful fraction of prospects who start the signup flow (post #4) don't finish it in one sitting. They complete eligibility, maybe start intake, and then life interrupts. Without intervention, a large share of those never come back, not because they decided against the practice but because the thread got dropped. Recovering even a fraction of them is the highest-ROI automation available, because these are people who already self-selected as interested and eligible.
The sequence is a four-stage cron that runs daily and checks for patients stuck in an incomplete state. Stage one fires a gentle reminder a day or two after they stall ("you started getting set up, here's the link to pick up where you left off"). Stage two, a few days later, addresses the most common reason people stall, with a short note about what to expect and an offer to answer questions. Stage three, about a week in, is the last-chance nudge. Stage four, around the two-week mark, closes the loop: a final message, after which the lead is marked dormant and stops receiving the sequence.
The implementation is a state machine, not a queue of scheduled emails. Each patient row carries an onboarding-stage field and a last-stage-sent timestamp. The daily cron queries for patients whose state and elapsed time make them due for the next stage, sends that stage, and advances the field. Modeling it as state-plus-elapsed-time rather than as pre-scheduled individual emails matters because patients move: someone who completes intake after stage two should not receive stages three and four. With the state-machine model, completing intake moves them out of the incomplete state and the cron simply stops selecting them. With a pre-scheduled-email-queue model, you'd have to remember to cancel the queued messages, and the day you forget is the day a now-active patient gets a "come finish signing up" email.
Returning-patient discount and repurchase links
Two smaller revenue features that are worth the lines they take.
The returning-patient discount handles a patient who completed a course, lapsed, and comes back. Re-onboarding a known patient should not feel like starting from zero, and the application recognizes the returning patient (post #4 covered the detection) and applies a returning-patient price rather than the full new-patient initial-evaluation price, because the clinical work of re-establishing care with someone whose history is already on file is genuinely less than a cold start. The discount is implemented as a Stripe coupon applied automatically when the returning-patient state is detected, not as a separate product, so the accounting stays clean.
The repurchase link is a one-click path for an existing patient to buy another unit of something they've bought before: another treatment course, a maintenance renewal, a documentation letter. The patient gets an email (via the post-#6 SES pattern) with a button that goes to a pre-filled Stripe Checkout session for exactly the thing they're repurchasing. No navigating the portal, no re-selecting the product, no re-entering anything. For recurring purchases by established patients, removing the friction between intent and payment measurably increases completion. The link carries a signed token that identifies the patient and the product so the checkout session can be constructed server-side without trusting any client-supplied price.
Coupons
Coupons are a Stripe primitive, and the application uses them for the returning-patient discount above, for occasional promotional campaigns, and for specific partnership and outreach codes.
The mechanics are the same for any coupon: a code maps to a Stripe coupon object with a defined discount, the patient enters it at checkout or it's applied automatically via a campaign link, and the application records which coupon was used on the resulting payment so the campaign can be measured later.
The implementation detail that matters: validate coupons server-side at checkout-session creation, never trust a client-asserted discount. The client can say "apply this code"; the server looks up whether the code is a real, active, applicable coupon and constructs the Stripe Checkout session with the verified discount. A client that posts a fabricated discount gets nothing, because the discount is never read from the client; it's resolved server-side from the coupon code against Stripe's record. This is the same principle as the repurchase link's signed token: prices and discounts are resolved on the server, never accepted from the browser.
The reporting side of coupons is where they earn their keep. Because every payment records the coupon used, I can answer "how much revenue came through a given outreach code" and "what's the conversion rate on the returning-patient discount" without separate analytics instrumentation. The coupon field on the payment record is the attribution.
The tax question
Sales tax is the question most software-payment tutorials never mention and that a healthcare practice has to answer before it takes its first dollar. The good news for a medical practice is that the answer is usually simpler than it is for a typical e-commerce business.
Professional medical services are exempt from sales tax in the states the practice operates in (Florida and New Jersey, in my case). The practice provides medical services, not taxable goods, so it does not charge sales tax on treatment courses, memberships, or the clinical work that makes up the product matrix. The application's checkout flow reflects that: no tax line, no per-state rate computation, no tax-collection machinery, because there is no sales tax to collect on exempt medical services.
I want to be careful about how I frame this, because tax treatment is genuinely a question for a tax professional and not for a software architecture blog. The exemption for medical services is well established, but the boundaries (what counts as a taxable good versus an exempt service, how a specific state treats a specific line item) are the kind of thing you confirm with an accountant who knows your situation, not something you infer from a developer tutorial. I had that conversation early, confirmed the services are exempt, and built the checkout accordingly.
The architectural lesson is the inverse of most "handle tax correctly" advice: figure out whether you owe tax at all before you build any tax machinery. For a lot of software businesses the answer is "yes, in many jurisdictions, and you need a tax-computation service." For a medical practice selling exempt professional services, the answer can be "no," and the correct amount of tax code to write is none. The mistake would have been reflexively wiring up a tax-collection integration the product doesn't need. Confirm the obligation first; build only what the answer requires.
Next time
The next post covers compliance and backup: the HIPAA program across two state entities, the Business Associate Agreement registry as an operational discipline, the three-tier backup architecture formalized, the audit log, the backup-verification cron, and the incident-response playbook. The money layer is where bugs cost dollars; the compliance layer is where gaps cost far more than dollars. It's the part of the build that's least fun to write about and most important to get right.
