Single Bougie Free · Founder & Full-Stack Creator

This is what I built alone.
Imagine what I could build for you.

I'm Katurah Bobo — founder and full-stack creator of Single Bougie Free, a wellness brand running on a fully automated platform I designed and built from scratch while working full-time as a claims adjuster. Sign-ups, payments, and the monthly newsletter all run on their own. Now I build systems like this for creators with an audience. One woman built this. Imagine what I could build for you.

Automation Architect Payments & Email Automation Serverless on AWS Cost-Conscious Builds
See the work
Explore by Interest

Serverless by Design

Every piece of this pipeline was chosen intentionally. API Gateway handles intake, Lambda processes logic, DynamoDB stores members, SES delivers — and EventBridge fires the monthly batch automatically on the 1st. Zero servers to manage.

// Live architecture · production
Member onboarding flow
📋 Intake Form singlebougiefree.com
🔌 API Gateway REST · /prod/intake
λ Lambda Intake processor
🗄️ DynamoDB SBFMembers table
✉️ SES Welcome + current issue
Archive retrieval flow
🗂️ Archive Page singlebougiefree.com
🔌 API Gateway REST · /archive
λ Lambda sbf-archive-index
🪣 S3 + CloudFront sbf-newsletter · OAC
Resource Vault flow
📚 Resource Vault resources.html
🔌 API Gateway REST · /resources
λ Lambda sbf-resources-lambda
📊 Airtable Resource Leads table
Resource Vault paywall flow
📚 Resource Vault resources.html
🔌 API Gateway REST · /vault-access
λ Lambda sbf-vault-access-check
🗄️ DynamoDB SBFMembers
Monthly send flow
📅 EventBridge
1st of month
📊 Airtable
Content curation
λ Lambda Batch send processor
✉️ SES
🪣 S3 Archive
Tea Vault subscription flow
💳 Subscribe Button home.html · archive.html
🔌 API Gateway sbf-payments-api
λ Lambda sbf-create-checkout-session
🌐 Stripe Checkout $5/mo recurring
λ Lambda sbf-stripe-webhook
🗄️ DynamoDB SBFMembers · subscriptionStatus
API Gateway + Lambda
Why not a traditional server?
▾
The intake form gets bursts of submissions, not steady traffic. Serverless scales to zero between submissions — no idle EC2 cost.
Live
DynamoDB
Why not RDS?
▾
Member data is schemaless and read-heavy. DynamoDB's on-demand billing means I pay per request, not per hour for a running database instance.
Live
EventBridge + Batch Lambda
Why EventBridge?
▾
Removes the newsletter send from a human's to-do list entirely — it just goes out on its own every month. The original version was set to fire "every 30 days," which is like setting a timer instead of circling a date on a calendar: small gaps between months made it creep later and later. Caught during a review before it caused a problem, rebuilt to run on a fixed calendar date, and every subscriber branch (paying, past-due, free) has now been safely test-run and verified. Left switched off on purpose until next month's content is ready — a deliberate go-live, not an accidental one.
In Progress
Airtable as CMS
Why not a custom admin page?
▾
Airtable gives me a spreadsheet-style interface for curating 6 content sections without building a custom UI — keeping engineering effort where it matters.
Live
SES Welcome Email
Triggered on signup.
▾
New members receive the current newsletter immediately on intake — before being added to the EventBridge batch list.
Live
SES Sender Identity
Why did switching senders need zero new setup?
▾
AWS doesn't verify one email address at a time here — it verified the whole domain once, up front. So switching who an email appears to come from is just a settings change, not a new security check, as long as the new address still ends in singlebougiefree.com.
Live
A Rewind Button for Members
What happens if a bad update wipes the subscriber list?
▾
It can be rewound. The member database now keeps a running history of every change for 35 days, so it can be restored to any exact second before a mistake. It costs pennies a year at this size — cheap insurance for the one table that holds paying members.
Live
sbf-archive-index Lambda
Why not a database query?
▾
Queries the sbf-newsletter bucket and returns card-ready data to the front end. One Lambda replaces an entire CMS.
Live
CloudFront · Newsletter CDN
Why a separate distribution?
▾
Newsletter CDN layer with Origin Access Control. Newsletters are private in S3; CloudFront is the only way in.
Live
API Gateway · /archive Route
Why not a new endpoint?
▾
Extends the production API with a read-only archive endpoint, demonstrating multi-route API design on a single Gateway.
Live
CodePipeline · CI/CD
Full CI/CD pipeline:
▾
GitHub is the source of truth. CodePipeline detects every push, deploys to S3, and fires a CloudFront invalidation automatically — replacing a manual multi-step process with a single git push. Verified live across seven consecutive deploys.
Live
Stripe + API Gateway · sbf-payments-api
Why a dedicated API?
▾
Payment flows get their own Gateway rather than sharing the content API — isolating billing traffic and credentials from the newsletter/archive routes.
Live
sbf-stripe-webhook Lambda
Why a webhook, not polling?
▾
Stripe pushes subscription events (created, canceled, payment failed) the moment they happen. The webhook Lambda updates SBFMembers in real time instead of checking Stripe on a schedule.
Live
Secrets Manager · Stripe Credentials
Why not environment variables?
▾
Stripe API keys are pulled from Secrets Manager at runtime rather than stored in Lambda config — keeping payment credentials out of the codebase and IaC entirely.
Live
Mini / Full Newsletter Split
Why split the send?
▾
Free members get the mini newsletter; Tea Vault subscribers get the full send. Same EventBridge trigger, same Lambda — the split happens on subscriptionStatus, not a separate pipeline.
Live
sbf-resources-lambda
Why not a static list?
▾
Resources are tagged by Topic in Airtable and queried live — a new topic gets added by typing it into a spreadsheet, no code deploy required to grow the taxonomy.
Live
sbf-vault-access-check Lambda
Why a new Lambda instead of adding to an existing one?
▾
Every backend piece here does exactly one job with its own locked-down permissions. This one only checks a single email against a single table — giving it its own function keeps that permission narrow and leaves every other Lambda's access untouched.
Live
Letter Page Integration — Link, Not Generate
Why not auto-generate the letter from Airtable?
▾
The monthly personal letter has styled, multi-paragraph formatting that doesn't map cleanly onto a plain text field. Automating it would mean giving the newsletter Lambda write access to a second S3 bucket, or building a whole separate deploy path — real complexity for something that happens once a month. Instead, the letter stays hand-built and reviewed in git, and the pipeline (both newsletter templates, the archive API, and the archive page) just link to wherever it lives.
Live
One Setting, Two Purchase Paths
Why point both the on-site checkout AND the email Payment Link at the same env-var-driven page?
▾
The Lambda that creates a checkout session reads its redirect destination from an environment variable instead of a hardcoded value — and the separate Payment Link used in emails has its own "after payment" setting pointed at the same URL. One destination, two entry points, and changing where either one lands is a config edit, not a code deploy.
Live
sbf-visit-counter Lambda
Why an atomic increment instead of read-the-number-then-write-it-back?
▾
A visit counter that reads the current number, adds one in code, then saves it back has a race condition — two people loading a page at the exact same instant can both read "47" and both save "48," silently losing a visit. DynamoDB's atomic ADD operation increments the stored number in one indivisible step instead, so simultaneous visits are never dropped. Same one-Lambda-one-job pattern as the rest of this stack, with its own least-privilege role scoped to write access on a single new table, nothing else. Tracks both a per-page count and a site-wide total in one call. Deployed and tested end-to-end via a direct API call — confirmed both DynamoDB rows update atomically and the Lambda returns the live site total. Deliberately held out of the live pages for now: the footer display and the front-end tracking snippet only go in once this is ready to launch, so testing visits along the way don't inflate the real count.
In Progress

From Commit to Production

Continuous Delivery
GitHub push triggers CodePipeline, which hands off to CodeBuild, syncs to S3, and invalidates CloudFront. Live in under a minute.
Allowlist Build Configuration
Only front-end file types are included in every deploy — backend code can never reach the public bucket.
Least-Privilege IAM
The deploy role is scoped to one S3 bucket and one CloudFront distribution — nothing else in the account.
A Second Scoped Key
This credential can write, but it can't read. It can push new code to several backend functions and nothing else — proven when it was correctly blocked from even looking at the function it had just updated. Same least-privilege idea as the deploy role above, just applied somewhere else in the pipeline.

Backend deployment automation is next on the roadmap — stay tuned.

One Song, Start to Stream

An original anthem — written by me, produced with AI, and shipped to a live page on AWS. Here’s every decision along the way.

24 KB
Web cover, down from 3.75 MB
40
Lyric lines timed by hand
6
Parts you can loop
0
New AWS services needed
// Studio to stream · production
🎙️ Suno Pro AI production
⏱️ Lyric Timer Custom tool → .vtt
🐙 GitHub One commit
⚙️ CodePipeline CodeBuild · allowlist
🪣 S3 Private bucket · OAC
🌐 CloudFront CDN · cache refreshed
🎵 anthem.html Player + synced lyrics
Written by Me, Produced with AI
Problem: The site needed a flagship anthem — an original song, not stock music — without a studio budget.
▾
Fix: Wrote the lyrics, then produced the track in Suno on a Pro plan from the very first take, so every version carries commercial rights. Instead of regenerating the whole song to fix one weak spot, individual sections were repaired in place — matching each new line’s syllable count to the one it replaced so the melody held.
An Allowlist Doing Its Job
Problem: The song went live, but its cover art didn’t — and the page returned “Access Denied” instead of “Not Found.”
▾
Fix: The deploy only ships approved file types from approved folders, and the image was sitting outside them. Because the storage is private behind the CDN, a missing file looks like a locked door. Moved the image into the approved folder rather than loosening the rule — so a stray screenshot can never go public by accident.
3.75 MB → 24 KB
Problem: The 3000×3000 master cover is perfect for streaming platforms and far too heavy for a webpage.
▾
Fix: Created an 800×800 WebP version (24 KB) with a JPG fallback (59 KB) for older browsers. Every visitor now downloads about 150× less for the same picture — faster pages and less CDN data transfer to pay for. The audio follows the same idea: only the song’s length loads up front, and the full 4.4 MB file downloads when someone actually presses play.
Building a Tool to Time the Lyrics
Problem: Lyrics that highlight along with the song need a start time for every single line — and the audio file doesn’t contain any.
▾
Fix: Built a small private tap-along tool: play the song, tap the spacebar as each line begins, and it exports a WebVTT file — the same captions format behind video subtitles. All 40 lines were timed in one listen, and the tool is reusable for every monthly single.
Why Not Just Ship the Timing File?
Problem: The obvious move was to publish the timing file alongside the song — but it isn’t an approved file type, and browsers refuse to load it during local testing.
▾
Fix: Baked each line’s start time directly into the page instead. No change to the deploy rules, one less file for the CDN to serve, and it works the same on a laptop as it does live. The timing file stays in the repository as the master copy.
Smooth, Not Seasick
Problem: The first version re-centered the page on every new lyric — about 40 scrolls per song — and it felt jumpy.
▾
Fix: Now the page holds still while the highlight moves down the screen, and only glides once the current line nears the bottom: roughly 8 gentle scrolls per song instead of 40. It also steps aside when a listener scrolls on their own, and respects devices set to reduce motion.
Jump to Your Part
Problem: My daughter loved the Anthem but only wanted to replay her favorite part — which meant scrubbing the progress bar every time, even while driving.
▾
Fix: Added section buttons that play just that part and stop, plus a loop switch. No new data — each section’s start and end come from the lyric timings already on the page, so every future single gets the feature for free.
The Ear Beats the Waveform
Problem: The first stop points cut singers off mid-word. Timing marks show when a line starts, not when the last note ends.
▾
Fix: Added ring-out time and a short fade, then measured the song’s vocal loudness to find real gaps. The data found a silence at 2:32 — but the right ending by ear was 2:35 — so each section can now be hand-tuned on its own.
Share a Moment, Not a Link
Problem: Sending the song meant sending the whole song — and a bare link in a text looks like spam.
▾
Fix: Every part has its own link (anthem.html#bridge) that opens ready to play with a “Hey Sis, this Bridge is for you” note. Phones get the built-in share menu, and link-preview tags turn a text into a card with the cover art.

Field Notes From Production

Stripe SDK Compatibility
Problem: A routine update to the payment software (Stripe) silently changed the structure of the data it returns after checkout — the existing code expected the old structure and broke.
▾
Fix: Rewrote that small piece of code to read the new format correctly.
Environment Variable Mismatch
Problem: One piece of backend code was looking for a setting using the wrong name, so it crashed every single time it tried to run.
▾
Fix: Corrected the name it was looking for, and checked every other piece of code for the same mistake.
Public Content Exposure
Problem: A public webpage had a direct link to content that was supposed to be paid-only, meaning anyone could find and read it for free without ever paying.
▾
Fix: That paid content is now only ever sent through a private email, after someone actually pays.
A Link That Pointed at Nothing
Problem: A menu link was supposed to open a pop-up window, but that pop-up didn't actually exist on that page — so clicking it did nothing, with no error message to catch the mistake.
▾
Fix: Pointed the link to the page that actually has that pop-up, so it opens automatically the moment that page loads.
Catching a Quiet Drift Before It Sent
Problem: A monthly automated email was set to repeat "every 30 days" — but since months aren't all the same length, its actual send date kept slipping later, unnoticed, until it was two weeks from an unplanned send to every subscriber.
▾
Fix: Paused it immediately, then rebuilt it on a real calendar date — "the 1st of the month" instead of "every 30 days" — so it can never drift again. Kept it switched off until a full test pass and a fresh month's content were both ready.
A Status That Didn't Quite Match
Problem: While safely test-running that same monthly email before turning it back on, an account marked "payment past due" should have been skipped entirely — instead it got sent the free version, with no error to flag it. Its status had been typed with a space instead of an underscore, so the code's exact-match check never recognized it and quietly treated it like any other unrecognized account.
▾
Fix: Corrected the stored value and re-ran the test — confirmed the account was properly skipped this time, exactly as intended.
One-Time Ticket vs. Standing Reservation
Problem: Not every "pay now" button can work the same way. The site's checkout button works like a ticket printed the moment you ask for it — good for one use, then it's void — which is great for a live webpage but useless for a link inside an email that might sit unread for weeks.
▾
Fix: Set up a second, permanent payment link instead — like a standing reservation that's always good — built specifically for the two email spots, while the live-page checkout keeps using its one-time-ticket approach.
A Renamed File, A Broken Deploy
Problem: A backend file gets a clearer, more descriptive name for everyday editing — but the function itself was still configured to look for its original file name, so every deploy under the new name crashed instantly on the next run, with no actual bug in the code.
▾
Fix: Repackaged the deploy to include the file under the exact name the function expects, keeping the friendlier name for local editing only.
A Cleanup Step That Happened Anyway
Problem: The last step in a short checklist for publishing new code was supposed to delete a leftover file — but it happened even though an earlier step on that same checklist had already failed, so it deleted a file that was still needed before its replacement even existed.
▾
Fix: Linked every step together so if one fails, none of the following ones happen either — instead of the last step going ahead no matter what came before it.
Cleaning Up an Old Exposure
Problem: An early version of this project accidentally made a backend file public with an AWS account number typed directly into it.
▾
Fix: That value now lives in the function's settings instead of the source code, closing a gap that had been sitting on the to-do list since the very first cleanup pass.
A Near Match, Not a Match
Problem: An environment variable pointed the checkout redirect at subscribe-success.html — a page that didn't exist; the real file was named success.html. It looked right at a glance.
▾
Fix: Caught it during review before saving, corrected the value to match the actual filename, and verified the live page loaded.
A Background That Wouldn't Come Clean
Problem: Pulling the teacup icon straight out of the real brand artwork by digitally erasing its background worked for thin outline strokes, but any solid-colored area — the cup's body, the tea itself — kept blending into its own softened surroundings and disappearing, leaving the icon looking hollow or cut off on one side.
▾
Fix: Rebuilt the background estimate using only genuine background pixels reconstructed around the icon's outline, which fixed the hollow-fill problem — but ultimately the raster cutout was dropped entirely in favor of an original, hand-drawn icon (see Front-End Stack, below) for a cleaner result at every size.
Three Links to a Page That Moved Twice
Problem: Three buttons in the unsubscribe email flow pointed at a homepage address that had already been retired. That had been corrected once before, in an earlier session — pointed at a different page instead — but the fix was never actually saved to the project's permanent history, and that second page was also retired later on. The half-saved fix sat quietly wrong for weeks until a routine status check turned it up.
▾
Fix: Pointed all three links at the site's real, current homepage, and saved the change properly this time.
Access Denied, On Purpose
Problem: Anyone who typed a wrong link got a cold "Access Denied" message instead of a normal "page not found." That's because the site's files are locked down so tightly that they won't even say whether a page exists — great for security, confusing for a visitor.
▾
Fix: Left the lock exactly as it was, and set up the site to show a friendly, on-brand "This page spilled somewhere else" page whenever someone lands on a wrong link.
The Webhook That Never Said No
Problem: When someone paid, Stripe sent a message to the site saying "add this new member." The site answered "got it" every single time — even when saving the new member actually failed. So Stripe never tried again, and the error alarm never went off. A paying customer could have been charged without ever getting access, and nobody would have known.
▾
Fix: The site now answers honestly. If saving fails, it says so — Stripe automatically tries again for up to a few days, and I get an alert right away. Emails only go out after the save succeeds, so a retry never sends anyone a duplicate.
The Pop-Up With No Exit
Problem: On a phone, the contact pop-up was taller than the screen and centered on it — so its close button sat just off the top edge, and with scrolling switched off, there was no way out.
▾
Fix: Made the pop-up scrollable and anchored it to the top when it's too tall to fit, so the close button is always within reach.
The Form That Was Quietly Losing Leads
Problem: The contact form asked visitors for their phone number, business name, and why they were reaching out — but the system behind it only kept their name, email, and message. Everything else was silently thrown away. A potential client could fill out the whole form, and I'd never see how to call them back.
▾
Fix: The form now saves and emails every detail it asks for. New client inquiries arrive with a clear "New lead" subject, come from my business address, and hitting Reply goes straight to the person who wrote in.
Behind-the-Scenes Code, Left on Stage
Problem: The code that runs the contact form was stored in the same folder as the website's pages — so it got published right along with them. Anyone who guessed the address could read it. No passwords were inside, but my email addresses were, and nothing behind the scenes belongs on a public page.
▾
Fix: Moved it into the folder where all the other behind-the-scenes code lives, which never gets published. The next update automatically removed the public copy.

Additional entries will be added as new issues are diagnosed and resolved — stay tuned.

Consistent by Design

A shared design system — one set of fonts, colors, and components — used across every page.

Page Consolidation
Why one page instead of three?
▾
single.html, bougie.html, and free.html were merged into a single About/Philosophy page with anchor sections — one template to maintain instead of three near-duplicates, and one first-person manifesto instead of three separate landing pages.
Transparent Asset Handling
Why verify the alpha channel before handoff?
▾
A dark-background logo file left a faint rectangular edge against the hero's gradient. Swapping in a verified transparent-background PNG — not just a white-background one — resolved it with zero HTML changes, since the filename and every reference stayed the same.
One Popup, Reused Everywhere
Why link to another page instead of copying the popup?
▾
Only one page has the real "Share Feedback" and "Let's Talk Tech" popups actually built into it. Every other page just links to it with a small tag at the end of the URL (like #feedback) — that page reads the tag the moment it loads and pops the window open automatically. One popup to update, not five copies to keep in sync.
Custom Brand Icon System
Why a hand-drawn icon instead of an extracted one?
▾
Every teacup mark across the site used to be a generic emoji standing in for the brand. Replacing it started with trying to lift the icon straight out of the real brand artwork (see Troubleshooting, above) — that path kept leaving artifacts at UI scale. Redrawing it as a simple, original SVG and recoloring it in the brand's signature pink solved it cleanly: one small file, applied consistently everywhere the old emoji appeared, rendering identically from a small badge to a large modal icon at any size.
One API, Not Five SDKs
Why not build separate Facebook/Twitter/Instagram share buttons?
▾
The Letter from Katurah pages use the browser's own Web Share API (navigator.share) instead of platform-specific share buttons. One line of code hands off to whatever the reader's device already has installed — Messages, Instagram, Mail, WhatsApp — with zero SDKs, API keys, or icon sets to maintain. Feature-detected with a clipboard-copy fallback for desktop browsers that don't support it yet, so the button still works everywhere, just differently.
Playfair Display DM Sans DM Mono CSS Custom Properties Responsive Grid Reusable Components Web Share API

Capabilities, Backed by Proof

Every skill below points at the actual live service that demonstrates it — not a self-rated bar, just working production proof.

Serverless Architecture

Proof: the full intake → archive → resources → monthly-send pipeline runs entirely on API Gateway, Lambda, DynamoDB, and EventBridge — zero servers managed.

Live
Least-Privilege IAM

Proof: every Lambda gets its own scoped role — sbf-resources-lambda-role, for example, can read exactly one Secrets Manager secret and nothing else in the account.

Live
Payment & Subscription Integration

Proof: Stripe Checkout → webhook Lambda → DynamoDB keeps subscriptionStatus in sync in real time, tested end-to-end including cancellations and past-due states.

Live
Secrets Management & Incident Response

Proof: when a token was accidentally exposed in a config output, it was rotated, the secret updated, and dead references stripped from every affected Lambda the same session.

Live
CI/CD Automation

Proof: GitHub → CodePipeline → CodeBuild → S3 + CloudFront deploys every push automatically, verified live across nine consecutive releases.

Live
Cost-Conscious Builds

Proof: every service choice has a documented cost reason — Lambda over servers, pay-per-use over always-on — so this whole platform runs for under $5 a month, with a budget alert watching it.

Live

This list grows as new pieces of the stack ship.

The System Behind SBF, Built for You

Every month, SBF sends a free newsletter and a paid one without me lifting a finger. I build that same system for creators on your own AWS and Stripe accounts, so you own it and keep every subscription dollar. You bring the content. I build the system that runs it.

What you get
💎

Free + paid tiers

Readers sign up free, and members who pay get the full issue.

💳

Monthly subscriptions

Stripe bills your members every month and handles cancellations on its own.

📤

Upload & auto-send

Drop in both versions each month, and each issue goes to the right list automatically.

🔐

Yours to keep

Everything lives in your own accounts, and you keep 100% of subscription revenue.

How it works
STEP 1

Discovery call

30 minutes, no pressure.

STEP 2

Proposal & deposit

A written plan within 5 business days, and 50% to start.

STEP 3

Build & test

Built in your accounts and tested before launch.

STEP 4

Launch & handoff

Final 50% at launch, plus a guide, walkthrough, and 14 days of support.

Tell me what you need.

Contact

Built Solo.
Ready for Yours.

Whether you're a creator with an audience or just curious how this all works — I'd love to hear from you.

⚙️
Thank you.
Your message has been received.
— Katurah
🩷
Thanks Sis!
The tea has been received.
We see you and we appreciate you.
Love, Katurah