<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mohamed Sadok</title>
    <description>The latest articles on DEV Community by Mohamed Sadok (@mohamed_sadok_42edff3a1b3).</description>
    <link>https://dev.to/mohamed_sadok_42edff3a1b3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2130609%2F94d003fd-e2e2-475d-9b08-b326062707fe.jpg</url>
      <title>DEV Community: Mohamed Sadok</title>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohamed_sadok_42edff3a1b3"/>
    <language>en</language>
    <item>
      <title>I Built a Tool to Find the Tests Developers Have Stopped Trusting</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Tue, 29 Sep 2026 08:36:54 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-tool-to-find-the-tests-developers-have-stopped-trusting-3j3c</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-tool-to-find-the-tests-developers-have-stopped-trusting-3j3c</guid>
      <description>&lt;p&gt;I Built a Tool to Find the Tests Developers Have Stopped Trusting&lt;/p&gt;

&lt;p&gt;A test fails in CI.&lt;/p&gt;

&lt;p&gt;You look at it.&lt;/p&gt;

&lt;p&gt;You run it again.&lt;/p&gt;

&lt;p&gt;It passes.&lt;/p&gt;

&lt;p&gt;You run it again.&lt;/p&gt;

&lt;p&gt;It passes.&lt;/p&gt;

&lt;p&gt;You rerun the pipeline.&lt;/p&gt;

&lt;p&gt;Everything is green.&lt;/p&gt;

&lt;p&gt;So you merge the pull request.&lt;/p&gt;

&lt;p&gt;A few hours later, the same test fails again.&lt;/p&gt;

&lt;p&gt;At some point, the team stops treating that failure as useful information.&lt;/p&gt;

&lt;p&gt;That's the real problem with flaky tests.&lt;/p&gt;

&lt;p&gt;They don't just make CI unreliable.&lt;/p&gt;

&lt;p&gt;They make developers stop trusting CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't writing tests
&lt;/h2&gt;

&lt;p&gt;Modern JavaScript projects already have excellent testing tools.&lt;/p&gt;

&lt;p&gt;Playwright is great for browser testing.&lt;/p&gt;

&lt;p&gt;Vitest and Jest are great for unit and integration testing.&lt;/p&gt;

&lt;p&gt;CI platforms can execute thousands of tests.&lt;/p&gt;

&lt;p&gt;The problem starts after the tests exist.&lt;/p&gt;

&lt;p&gt;A growing project eventually accumulates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests that fail intermittently&lt;/li&gt;
&lt;li&gt;Tests that take too long&lt;/li&gt;
&lt;li&gt;Tests that haven't failed in months&lt;/li&gt;
&lt;li&gt;Tests that fail only in CI&lt;/li&gt;
&lt;li&gt;Tests that fail because of timing&lt;/li&gt;
&lt;li&gt;Tests that produce almost identical errors&lt;/li&gt;
&lt;li&gt;Tests that nobody wants to touch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test suite becomes a second system that needs maintenance.&lt;/p&gt;

&lt;p&gt;And most teams don't have a good way to measure its health.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built TestOps Kit
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't replace the test runner. Analyze what it produces.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TestOps Kit consumes machine-readable test results and builds a reliability layer around them.&lt;/p&gt;

&lt;p&gt;The workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Playwright / Vitest / Jest
            ↓
       Test results
            ↓
       TestOps Kit
            ↓
 ┌──────────┼───────────┐
 ↓          ↓           ↓
History   Reliability  Performance
 ↓          ↓           ↓
Flaky     Quarantine   Slow tests
tests
            ↓
        Dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The existing testing workflow stays intact.&lt;/p&gt;

&lt;p&gt;TestOps simply adds another layer of information.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first feature I wanted was flaky-test detection
&lt;/h2&gt;

&lt;p&gt;A single failed test doesn't necessarily mean a test is flaky.&lt;/p&gt;

&lt;p&gt;You need history.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run 1 → PASS
Run 2 → PASS
Run 3 → FAIL
Run 4 → PASS
Run 5 → FAIL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That pattern is much more interesting than a single failure.&lt;/p&gt;

&lt;p&gt;TestOps tracks test behavior across runs and calculates reliability metrics from the available history.&lt;/p&gt;

&lt;p&gt;The goal isn't to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This test is definitely broken."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This test has demonstrated inconsistent behavior and deserves investigation."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a much more useful signal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then came quarantine
&lt;/h2&gt;

&lt;p&gt;Once you identify unreliable tests, the next problem is operational.&lt;/p&gt;

&lt;p&gt;What do you do with them?&lt;/p&gt;

&lt;p&gt;TestOps can generate a quarantine manifest based on a configurable flakiness threshold.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flaky tests

checkout/payment.spec.ts     42%
account/login.spec.ts        31%
orders/create.spec.ts        27%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of relying on someone's memory, the team now has a concrete list.&lt;/p&gt;

&lt;p&gt;The important part is that quarantine is treated as a temporary reliability workflow, not a way to permanently hide failures.&lt;/p&gt;
&lt;h2&gt;
  
  
  Slow tests are another form of test debt
&lt;/h2&gt;

&lt;p&gt;A test doesn't have to fail to become expensive.&lt;/p&gt;

&lt;p&gt;Imagine a suite containing:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,200 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and a handful of tests account for a significant portion of the runtime.&lt;/p&gt;

&lt;p&gt;Those tests affect every developer.&lt;/p&gt;

&lt;p&gt;Every pull request.&lt;/p&gt;

&lt;p&gt;Every CI run.&lt;/p&gt;

&lt;p&gt;Every deployment.&lt;/p&gt;

&lt;p&gt;So TestOps also tracks duration and highlights slow tests.&lt;/p&gt;

&lt;p&gt;The goal is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find the tests that are costing the team time.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Visual regression belongs in the same conversation
&lt;/h2&gt;

&lt;p&gt;Testing isn't only about pass and fail.&lt;/p&gt;

&lt;p&gt;UI changes can also introduce unexpected regressions.&lt;/p&gt;

&lt;p&gt;TestOps includes deterministic snapshot checking so a CI pipeline can detect changed snapshot content.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx testops snapshot snapshots &lt;span class="nt"&gt;--strict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If a snapshot changes unexpectedly, the command returns a failure.&lt;/p&gt;

&lt;p&gt;That makes it possible to use the same reliability workflow in CI.&lt;/p&gt;
&lt;h2&gt;
  
  
  I also wanted a boring CLI
&lt;/h2&gt;

&lt;p&gt;Developer tools don't need complicated installation procedures.&lt;/p&gt;

&lt;p&gt;The basic workflow should be something like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx testops analyze &lt;span class="nt"&gt;--input&lt;/span&gt; test-results.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx testops report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And you get a dashboard.&lt;/p&gt;

&lt;p&gt;The dashboard focuses on the questions developers actually care about:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How many tests do we have?

How many are failing?

Which ones are flaky?

Which ones are slow?

What happened in previous runs?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Testing the tester
&lt;/h2&gt;

&lt;p&gt;One of the most important parts of building a developer tool is testing it against something real.&lt;/p&gt;

&lt;p&gt;A synthetic demo can prove that the software works in theory.&lt;/p&gt;

&lt;p&gt;It doesn't prove that developers can use it in their projects.&lt;/p&gt;

&lt;p&gt;So the validation workflow is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing project
      ↓
Existing tests
      ↓
Generate real test report
      ↓
Feed report into TestOps
      ↓
Compare metrics
      ↓
Create intentional flaky test
      ↓
Run repeatedly
      ↓
Verify detection
      ↓
Create slow test
      ↓
Verify performance detection
      ↓
Modify snapshot
      ↓
Verify regression detection
      ↓
Run inside CI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the test that matters.&lt;/p&gt;

&lt;p&gt;Not whether the landing page looks good.&lt;/p&gt;

&lt;p&gt;Not whether the dashboard has impressive charts.&lt;/p&gt;

&lt;p&gt;Whether it can survive a real repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The interesting part of test infrastructure isn't necessarily generating more tests.&lt;/p&gt;

&lt;p&gt;It's understanding the tests you already have.&lt;/p&gt;

&lt;p&gt;A team can have 5,000 tests and still have a reliability problem.&lt;/p&gt;

&lt;p&gt;More tests don't automatically mean more confidence.&lt;/p&gt;

&lt;p&gt;Sometimes the real question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which tests can we trust?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the problem TestOps Kit is designed around.&lt;/p&gt;
&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;The long-term idea is bigger than a CLI.&lt;/p&gt;

&lt;p&gt;A mature version could understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test history&lt;/li&gt;
&lt;li&gt;CI environments&lt;/li&gt;
&lt;li&gt;Failure patterns&lt;/li&gt;
&lt;li&gt;Code changes&lt;/li&gt;
&lt;li&gt;Test ownership&lt;/li&gt;
&lt;li&gt;Dependency relationships&lt;/li&gt;
&lt;li&gt;Visual changes&lt;/li&gt;
&lt;li&gt;Performance regressions&lt;/li&gt;
&lt;li&gt;Failure clusters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, the system could answer questions like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which tests became unreliable after this deployment?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which changed files are responsible for most of the tests we need to run?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which tests have consumed the most CI time this month?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the first version starts with something much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure test reliability instead of guessing about it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because once developers stop trusting their test suite, the test suite has already become technical debt.&lt;/p&gt;

&lt;p&gt;And that's a problem worth measuring.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://boukataya.gumroad.com/l/ivsbba" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2Fbu0po2cp9j7py75bgsz9s4wfkyx9" height="495" class="m-0" width="1005"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://boukataya.gumroad.com/l/ivsbba" rel="noopener noreferrer" class="c-link"&gt;
            TestOps Kit — Flaky Test Detection, Test Reliability &amp;amp; CI Toolkit
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            TestOps KitProduction Test Reliability Toolkit for Modern JavaScript TeamsYour test suite is supposed to give you confidence.Instead, you get: Random CI failures Tests that pass locally but fail in CI Slow test suites Repeated failures nobody investigates Visual regressions Hundreds of test results to manually inspect Flaky tests that waste hours every week TestOps Kit turns raw test results into actionable test reliability data.It analyzes your existing test reports, tracks test history, detects flaky tests, identifies slow tests, generates quarantine manifests, checks visual snapshots, and produces a standalone reliability dashboard.No need to replace your existing test framework.What you getFlaky Test DetectionTrack test behavior across multiple runs and identify tests that repeatedly switch between passing and failing.See: Flakiness percentage Failure frequency Run history Test duration Failure information Test Reliability DashboardGet a single view of your test suite: Total tests Pass rate Failed tests Flaky tests Slow tests Test duration Historical runs Failure details Test QuarantineGenerate a quarantine manifest for tests that exceed your configured flakiness threshold.Instead of manually maintaining a list of unreliable tests, let TestOps identify them from actual test history.Slow Test DetectionFind tests that are consuming disproportionate CI time.Use duration history to identify tests that deserve optimization.Visual Regression ChecksCreate deterministic snapshot baselines and detect unexpected changes using content hashing.Useful for: HTML snapshots JSON snapshots Generated assets Test fixtures Visual regression workflows CI ReadyDesigned to fit into existing CI pipelines.Use it with GitHub Actions and your existing test infrastructure.Multiple Report FormatsThe toolkit supports common machine-readable test reports including: Playwright JSON Vitest/Jest-style JSON JUnit XML Test Impact AnalysisIdentify candidate tests related to changed files so you can focus testing where changes actually occurred.Stress TestingRun the same command repeatedly to expose intermittent failures.Example:npx testops stress --command "npm test" --runs 20 Developer CLIEverything is accessible from the command line.npx testops analyze --input test-results.json npx testops report npx testops quarantine npx testops snapshot snapshots --strict npx testops stress --command "npm test" --runs 20 Built for JavaScript developers TypeScript developers SaaS teams Freelancers Agencies QA engineers DevOps engineers CI/CD-heavy projects Teams using Playwright, Vitest or Jest Requirements Node.js 18+ npm Git Existing automated test reports Included TestOps CLI Reliability analyzer Flaky test detector Test history Quarantine generator Slow-test analysis Stress runner Visual snapshot checker Git impact analysis Standalone dashboard GitHub Actions integration Demo fixtures Complete documentation Example configuration MIT license Why TestOps?Test generation is only half the testing problem.The other half is knowing whether the tests you already have can actually be trusted.TestOps focuses on that problem.Know which tests are stable. Know which ones are flaky. Know which ones are slowing CI down.Build with confidence instead of debugging CI noise.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2F7ai3z4dx2bl2bs70di842sywirml" width="400" height="250"&gt;
          boukataya.gumroad.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>testing</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How We Built a Production-Ready SaaS Billing Engine (And How You Can Ship Yours Today)</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:53:44 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/how-we-built-a-production-ready-saas-billing-engine-and-how-you-can-ship-yours-today-4be3</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/how-we-built-a-production-ready-saas-billing-engine-and-how-you-can-ship-yours-today-4be3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dz4oows9nti7hokk3q9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dz4oows9nti7hokk3q9.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
The complete blueprint for handling subscriptions, customer billing portals, and edge-case webhooks without losing your sanity.*&lt;/p&gt;



&lt;p&gt;Every developer building a SaaS eventually runs into the same wall: &lt;strong&gt;Monetization Architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Building the core features of an application is exciting. Writing billing logic, managing recurring cycles, handling failed credit card charges, generating PDFs, and wiring up webhook endpoints is usually where momentum slows down.&lt;/p&gt;

&lt;p&gt;In this article, we'll walk through the architecture of a production-ready SaaS billing engine and examine the critical patterns needed to accept recurring payments reliably.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. The Billing State Problem
&lt;/h2&gt;

&lt;p&gt;Handling payments is not just a single API call to &lt;code&gt;charges.create()&lt;/code&gt;. A production billing system must manage state transitions across multiple scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when a renewal payment fails at 3:00 AM?&lt;/li&gt;
&lt;li&gt;How do you handle plan upgrades with prorated charges mid-cycle?&lt;/li&gt;
&lt;li&gt;What happens if a user cancels their subscription before the cycle ends?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The Core Subscription State Machine
&lt;/h3&gt;

&lt;p&gt;A resilient subscription model requires tracking distinct statuses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trialing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;active&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;past_due&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canceled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;incomplete&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Checkout ] ──► [ Active ] ──► (Cycle Renewal)
                      │                │
                      ▼                ▼
                 [ Canceled ]    [ Past Due ] ──► [ Inactive ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Webhooks: The Single Source of Truth
&lt;/h2&gt;

&lt;p&gt;Never rely solely on client-side redirect URLs (e.g., &lt;code&gt;/checkout/success&lt;/code&gt;) to grant access to paid tiers. Network disconnects, closed browser tabs, or client-side crashes can prevent access from being granted.&lt;/p&gt;
&lt;h3&gt;
  
  
  Recommended Webhook Pipeline
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify Signature:&lt;/strong&gt; Ensure the payload originated from your payment gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency Check:&lt;/strong&gt; Store incoming &lt;code&gt;event_id&lt;/code&gt; values to prevent processing duplicate events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Processing:&lt;/strong&gt; Immediately return a &lt;code&gt;200 OK&lt;/code&gt; response to the payment provider and dispatch the actual processing task to a background worker.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: Verifying and dispatching webhooks asynchronously&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;handleWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;hasBeenProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'already_processed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;ProcessBillingEventJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Customer Self-Service: The Billing Portal
&lt;/h2&gt;

&lt;p&gt;Modern users expect to manage their own billing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updating expired credit cards&lt;/li&gt;
&lt;li&gt;Downloading past VAT / Tax invoices&lt;/li&gt;
&lt;li&gt;Upgrading or downgrading plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using hosted billing portals (like Stripe Customer Portal) eliminates the need to build custom PCI-compliant credit card updating forms on your frontend.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Get the Starter Kit
&lt;/h2&gt;

&lt;p&gt;Rather than spending 40+ hours wiring up database schemas, webhook listeners, environment configs, and test suites, you can grab the complete &lt;strong&gt;SaaS Billing Engine Starter Kit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete source code with local DDEV / Docker support&lt;/li&gt;
&lt;li&gt;Production-tested webhook handlers&lt;/li&gt;
&lt;li&gt;Seeders, migrations, and plan definitions&lt;/li&gt;
&lt;li&gt;Detailed documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://boukataya.gumroad.com/l/shouoa" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/shouoa&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://saa-s-billing-starter-kit-demo.vercel.app/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;saa-s-billing-starter-kit-demo.vercel.app&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>laravel</category>
      <category>stripe</category>
      <category>saas</category>
      <category>php</category>
    </item>
    <item>
      <title>How I Built a 1970s Retro Vinyl Store Web App in 1 Single HTML File (No Frameworks, No Build Step)</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:17:29 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-a-1970s-retro-vinyl-store-web-app-in-1-single-html-file-no-frameworks-no-build-step-1717</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-a-1970s-retro-vinyl-store-web-app-in-1-single-html-file-no-frameworks-no-build-step-1717</guid>
      <description>&lt;p&gt;Every modern e-commerce website seems to look the same.&lt;/p&gt;

&lt;p&gt;Clean white backgrounds.&lt;br&gt;
Minimal cards.&lt;br&gt;
Rounded buttons.&lt;br&gt;
The same fonts.&lt;br&gt;
The same SaaS-style layouts.&lt;/p&gt;

&lt;p&gt;It works, but sometimes it feels completely soulless.&lt;/p&gt;

&lt;p&gt;So I decided to build something different.&lt;/p&gt;

&lt;p&gt;I wanted to recreate the feeling of walking into a small record store in the late 1970s — old vinyl sleeves, warm paper textures, bold typography, imperfect print effects, and even the subtle sound of vinyl crackling in the background.&lt;/p&gt;

&lt;p&gt;The result is GrooveHaus, a retro vinyl store web app built entirely inside one HTML file.&lt;/p&gt;

&lt;p&gt;No framework.&lt;br&gt;
No build process.&lt;br&gt;
No node_modules.&lt;br&gt;
No complicated setup.&lt;/p&gt;

&lt;p&gt;Just HTML, CSS, JavaScript, and a lot of experimentation.&lt;/p&gt;

&lt;p&gt;🎧 Live Demo &amp;amp; Template&lt;/p&gt;

&lt;p&gt;Live Demo:&lt;br&gt;
&lt;a href="https://groove-haus.vercel.app/" rel="noopener noreferrer"&gt;https://groove-haus.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Template:&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/ytowj" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/ytowj&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Idea Behind GrooveHaus&lt;/p&gt;

&lt;p&gt;The goal wasn't simply to make an "old-looking" website.&lt;/p&gt;

&lt;p&gt;I wanted the interface itself to feel like a physical object.&lt;/p&gt;

&lt;p&gt;Think:&lt;/p&gt;

&lt;p&gt;Vintage record sleeves&lt;br&gt;
Printed paper textures&lt;br&gt;
1970s-inspired typography&lt;br&gt;
Burnt orange, mustard, cream and burgundy colors&lt;br&gt;
Grain and imperfect print effects&lt;br&gt;
Records that physically slide out of their sleeves&lt;br&gt;
Analog vinyl crackle&lt;br&gt;
Simple page transitions&lt;br&gt;
A store experience that feels more like browsing a record shop than using a typical e-commerce dashboard&lt;/p&gt;

&lt;p&gt;The interesting part was trying to create those sensations using only browser-native technologies.&lt;/p&gt;

&lt;p&gt;The Constraint: One HTML File&lt;/p&gt;

&lt;p&gt;I deliberately gave myself a strict constraint:&lt;/p&gt;

&lt;p&gt;Everything had to live inside a single .html file.&lt;/p&gt;

&lt;p&gt;That means no React.&lt;/p&gt;

&lt;p&gt;No Vue.&lt;/p&gt;

&lt;p&gt;No build system.&lt;/p&gt;

&lt;p&gt;No npm installation.&lt;/p&gt;

&lt;p&gt;No bundler.&lt;/p&gt;

&lt;p&gt;No component library.&lt;/p&gt;

&lt;p&gt;The page contains the markup, styling and JavaScript needed to run the application.&lt;/p&gt;

&lt;p&gt;External resources are limited to things like fonts and CDN-hosted libraries.&lt;/p&gt;

&lt;p&gt;This sounds restrictive, but it actually made the project more interesting.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;"Which framework should I use?"&lt;/p&gt;

&lt;p&gt;I could focus on:&lt;/p&gt;

&lt;p&gt;"How much can I build with the browser itself?"&lt;/p&gt;

&lt;p&gt;The Stack&lt;/p&gt;

&lt;p&gt;The technology stack is intentionally simple:&lt;/p&gt;

&lt;p&gt;HTML5 handles the structure and application views.&lt;/p&gt;

&lt;p&gt;Tailwind CSS via CDN provides the utility classes and makes it easy to create the visual system.&lt;/p&gt;

&lt;p&gt;Vanilla JavaScript handles routing, application state, rendering, filtering and interactions.&lt;/p&gt;

&lt;p&gt;Web Audio API generates the vinyl crackle effect directly in the browser.&lt;/p&gt;

&lt;p&gt;Google Fonts and Font Awesome provide typography and icons.&lt;/p&gt;

&lt;p&gt;There is no backend because the goal was to create a self-contained frontend template.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Making the Browser Sound Like Vinyl&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of my favorite parts of the project is the analog noise effect.&lt;/p&gt;

&lt;p&gt;Instead of shipping an audio file containing vinyl crackle, GrooveHaus generates the sound dynamically using the Web Audio API.&lt;/p&gt;

&lt;p&gt;The browser creates an audio buffer containing low-level random noise combined with occasional louder pops.&lt;/p&gt;

&lt;p&gt;The result is a subtle background texture that resembles an old record playing.&lt;/p&gt;

&lt;p&gt;The important part is keeping it subtle.&lt;/p&gt;

&lt;p&gt;The effect isn't supposed to become annoying background music. It's more like an atmospheric layer that you notice when you stop and listen.&lt;/p&gt;

&lt;p&gt;Users can also turn the effect on or off.&lt;/p&gt;

&lt;p&gt;This was a good reminder that the Web Audio API can do much more than simply play MP3 files.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Tiny SPA Without a Framework&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application behaves like a small single-page application.&lt;/p&gt;

&lt;p&gt;There are different views for things like:&lt;/p&gt;

&lt;p&gt;Storefront&lt;br&gt;
Catalog&lt;br&gt;
Product details&lt;br&gt;
Artists&lt;br&gt;
Journal&lt;br&gt;
Cart&lt;br&gt;
Settings&lt;/p&gt;

&lt;p&gt;Instead of installing a routing library, I used the browser's native URL hash.&lt;/p&gt;

&lt;p&gt;For example, different hash routes determine which application view should be displayed.&lt;/p&gt;

&lt;p&gt;When the URL changes, JavaScript hides the previous view, displays the appropriate one and renders the required content.&lt;/p&gt;

&lt;p&gt;It's obviously not a replacement for a full routing solution in a large production application.&lt;/p&gt;

&lt;p&gt;But for a self-contained template?&lt;/p&gt;

&lt;p&gt;It works surprisingly well.&lt;/p&gt;

&lt;p&gt;And there is something satisfying about realizing that you don't always need another dependency to solve a relatively small problem.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Making Vinyl Records Feel Physical&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of the visual details I wanted was the feeling of pulling a record out of its sleeve.&lt;/p&gt;

&lt;p&gt;When hovering over a product card, the vinyl disc slides out from behind the album artwork while rotating.&lt;/p&gt;

&lt;p&gt;It's a small interaction, but it changes how the product card feels.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;Image → title → price → button&lt;/p&gt;

&lt;p&gt;you get something closer to interacting with a physical record.&lt;/p&gt;

&lt;p&gt;The effect is entirely CSS-based, using transforms and transitions.&lt;/p&gt;

&lt;p&gt;No animation library required.&lt;/p&gt;

&lt;p&gt;This is one of those cases where a few lines of CSS can create much more personality than another UI component ever could.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recreating Printed Paper&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Modern websites are usually extremely clean.&lt;/p&gt;

&lt;p&gt;GrooveHaus deliberately isn't.&lt;/p&gt;

&lt;p&gt;The background uses warm paper tones combined with subtle radial patterns to imitate the texture of printed material.&lt;/p&gt;

&lt;p&gt;The color palette is built around:&lt;/p&gt;

&lt;p&gt;Cream&lt;br&gt;
Aged paper&lt;br&gt;
Dark brown&lt;br&gt;
Burnt orange&lt;br&gt;
Mustard&lt;br&gt;
Burgundy&lt;/p&gt;

&lt;p&gt;Typography also plays a major role.&lt;/p&gt;

&lt;p&gt;Instead of using one generic UI font everywhere, the design combines display and serif typefaces to create something closer to vintage posters, record sleeves and printed magazines.&lt;/p&gt;

&lt;p&gt;The goal isn't perfect realism.&lt;/p&gt;

&lt;p&gt;It's controlled imperfection.&lt;/p&gt;

&lt;p&gt;Why I Didn't Use React&lt;/p&gt;

&lt;p&gt;This project probably could have been built faster with React.&lt;/p&gt;

&lt;p&gt;But that wasn't the point.&lt;/p&gt;

&lt;p&gt;Sometimes constraints are useful.&lt;/p&gt;

&lt;p&gt;Removing the framework forced me to think about what the browser already provides:&lt;/p&gt;

&lt;p&gt;DOM APIs&lt;br&gt;
URL hashes&lt;br&gt;
CSS transitions&lt;br&gt;
Web Audio&lt;br&gt;
browser storage&lt;br&gt;
native events&lt;br&gt;
modern JavaScript&lt;/p&gt;

&lt;p&gt;It also makes the final result extremely portable.&lt;/p&gt;

&lt;p&gt;There is something nice about downloading one HTML file and knowing that most of the application is sitting right there.&lt;/p&gt;

&lt;p&gt;Where Single-File Apps Actually Make Sense&lt;/p&gt;

&lt;p&gt;I'm not suggesting everyone should abandon frameworks.&lt;/p&gt;

&lt;p&gt;For a large SaaS platform, complex dashboard or production application with dozens of developers, a single HTML file would quickly become painful.&lt;/p&gt;

&lt;p&gt;But there are some cases where this architecture makes a lot of sense.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Templates&lt;/p&gt;

&lt;p&gt;A customer can download one file and immediately start customizing it.&lt;/p&gt;

&lt;p&gt;Prototypes&lt;/p&gt;

&lt;p&gt;You can experiment with an idea without spending time setting up infrastructure.&lt;/p&gt;

&lt;p&gt;Landing pages&lt;/p&gt;

&lt;p&gt;For a relatively simple marketing experience, a build pipeline may be unnecessary.&lt;/p&gt;

&lt;p&gt;Digital products&lt;/p&gt;

&lt;p&gt;A self-contained template is easy to distribute through marketplaces and digital stores.&lt;/p&gt;

&lt;p&gt;Experiments&lt;/p&gt;

&lt;p&gt;Sometimes you simply want to see how far native browser APIs can take you.&lt;/p&gt;

&lt;p&gt;The Bigger Lesson&lt;/p&gt;

&lt;p&gt;The most interesting part of building GrooveHaus wasn't actually the vinyl store.&lt;/p&gt;

&lt;p&gt;It was realizing how much you can do before reaching for another dependency.&lt;/p&gt;

&lt;p&gt;A browser already gives you a lot:&lt;/p&gt;

&lt;p&gt;HTML for structure.&lt;/p&gt;

&lt;p&gt;CSS for animation and visual effects.&lt;/p&gt;

&lt;p&gt;JavaScript for application logic.&lt;/p&gt;

&lt;p&gt;Web Audio for sound.&lt;/p&gt;

&lt;p&gt;The URL for simple routing.&lt;/p&gt;

&lt;p&gt;And modern browser APIs for everything in between.&lt;/p&gt;

&lt;p&gt;Frameworks are incredibly useful.&lt;/p&gt;

&lt;p&gt;But they aren't mandatory for every project.&lt;/p&gt;

&lt;p&gt;Sometimes putting yourself inside a few artificial constraints leads to more creative solutions.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;GrooveHaus started as a visual experiment:&lt;/p&gt;

&lt;p&gt;"Can I make a website feel like a physical record store?"&lt;/p&gt;

&lt;p&gt;It ended up becoming an experiment in something else:&lt;/p&gt;

&lt;p&gt;"How much can I build with nothing more than a browser and a single HTML file?"&lt;/p&gt;

&lt;p&gt;The answer is: quite a lot.&lt;/p&gt;

&lt;p&gt;And honestly, building something that doesn't look like another SaaS dashboard was probably the most refreshing part.&lt;/p&gt;

&lt;p&gt;Live Demo:&lt;br&gt;
&lt;a href="https://groove-haus.vercel.app/" rel="noopener noreferrer"&gt;https://groove-haus.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Template:&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/ytowj" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/ytowj&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What retro web design trend would you bring back?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjje21geymhoo0xd2mqah.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjje21geymhoo0xd2mqah.jpg" alt=" " width="799" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssjaax52nhc4yjg2rlmd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssjaax52nhc4yjg2rlmd.jpg" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcb6tzjrtskw4x43ngt3o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcb6tzjrtskw4x43ngt3o.jpg" alt=" " width="799" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4uv53eygwtde3qmk220k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4uv53eygwtde3qmk220k.jpg" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpac26r1dnri0vby1ii3z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpac26r1dnri0vby1ii3z.jpg" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx569qerxsuy7mdal9w6v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx569qerxsuy7mdal9w6v.jpg" alt=" " width="798" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy8812otlbyppwd91qzl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy8812otlbyppwd91qzl.jpg" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93elshgyt37fryizftew.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93elshgyt37fryizftew.jpg" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>node</category>
    </item>
    <item>
      <title>How I Built Multi-Tenant SaaS Architecture with Next.js, Supabase, and PostgreSQL RLS</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:38:14 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-multi-tenant-saas-architecture-with-nextjs-supabase-and-postgresql-rls-506k</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-multi-tenant-saas-architecture-with-nextjs-supabase-and-postgresql-rls-506k</guid>
      <description>&lt;p&gt;Building the first version of a SaaS application is usually straightforward.&lt;/p&gt;

&lt;p&gt;Building the part that makes sure &lt;strong&gt;Customer A can never access Customer B's data&lt;/strong&gt; is where things get serious.&lt;/p&gt;

&lt;p&gt;A typical B2B SaaS eventually needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Organizations&lt;/li&gt;
&lt;li&gt;Team members&lt;/li&gt;
&lt;li&gt;Roles and permissions&lt;/li&gt;
&lt;li&gt;Invitations&lt;/li&gt;
&lt;li&gt;Protected routes&lt;/li&gt;
&lt;li&gt;Tenant-scoped data&lt;/li&gt;
&lt;li&gt;Database-level security&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can implement all of this incrementally.&lt;/p&gt;

&lt;p&gt;But there is a problem with that approach:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy affects almost every layer of the application.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I wanted to build a reusable architecture where tenant isolation was part of the foundation rather than something added after the application had already grown.&lt;/p&gt;

&lt;p&gt;This article explains the architecture I used with &lt;strong&gt;Next.js, Supabase, TypeScript, and PostgreSQL Row-Level Security.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The basic architecture
&lt;/h2&gt;

&lt;p&gt;The core relationship is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 │
 ▼
Organization Membership
 │
 ▼
Organization
 │
 ├── Projects
 ├── Invitations
 └── Other Tenant Resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A user doesn't directly own the application's business data.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → Membership → Organization → Resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction becomes important when a user belongs to multiple organizations.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice
 ├── Acme Inc.       → Admin
 ├── Startup Labs    → Member
 └── Example Corp.   → Owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same authenticated user can therefore work across multiple tenants without creating separate accounts.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Row-Level Security Matters
&lt;/h1&gt;

&lt;p&gt;One of the easiest mistakes in a multi-tenant application is relying entirely on application-level filtering.&lt;/p&gt;

&lt;p&gt;Imagine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProjects&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer is expected to remember that every query must be scoped to the current organization.&lt;/p&gt;

&lt;p&gt;Usually that becomes something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getProjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is better.&lt;/p&gt;

&lt;p&gt;But there is still a problem.&lt;/p&gt;

&lt;p&gt;What happens when somebody forgets the filter?&lt;/p&gt;

&lt;p&gt;Or introduces a new API endpoint six months later?&lt;/p&gt;

&lt;p&gt;Or accidentally exposes a query through another code path?&lt;/p&gt;

&lt;p&gt;This is why I wanted PostgreSQL itself to enforce the tenant boundary.&lt;/p&gt;

&lt;p&gt;The goal becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
     │
     ▼
PostgreSQL
     │
     ▼
RLS Policy
     │
     ├── User belongs to organization → ALLOW
     │
     └── User does not belong → DENY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database becomes another security boundary.&lt;/p&gt;




&lt;h1&gt;
  
  
  Organizations and Memberships
&lt;/h1&gt;

&lt;p&gt;A simplified database model looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;profiles
   │
   │
   ▼
organization_members
   │
   ▼
organizations
   │
   ├── projects
   └── invitations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The membership table connects users to organizations.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organization_members

user_id
organization_id
role
created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the same user to have different roles in different organizations.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acme Inc.
Mohamed → OWNER

Startup Labs
Mohamed → MEMBER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The role belongs to the &lt;strong&gt;membership&lt;/strong&gt;, not globally to the user.&lt;/p&gt;

&lt;p&gt;That distinction makes the system much more flexible.&lt;/p&gt;




&lt;h1&gt;
  
  
  RBAC
&lt;/h1&gt;

&lt;p&gt;The starter uses three basic roles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OWNER
ADMIN
MEMBER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified permission model could look like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Permission&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;th&gt;Admin&lt;/th&gt;
&lt;th&gt;Member&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;View projects&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create projects&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage members&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change roles&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove members&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage organization&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important thing isn't the specific roles.&lt;/p&gt;

&lt;p&gt;It's keeping authorization centralized instead of scattering checks throughout the application.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;members.update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the application grows, additional roles and permissions can be introduced without rewriting every feature.&lt;/p&gt;




&lt;h1&gt;
  
  
  Invitations
&lt;/h1&gt;

&lt;p&gt;Invitations are another part of multi-tenancy that looks deceptively simple.&lt;/p&gt;

&lt;p&gt;The expected workflow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Admin
  │
  ▼
Create invitation
  │
  ▼
Generate token
  │
  ▼
Send invitation
  │
  ▼
User opens link
  │
  ▼
Verify invitation
  │
  ▼
Create membership
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The invitation record contains information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organization_id
email
token
role
expires_at
accepted_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation also verifies that the email associated with the invitation matches the account accepting it.&lt;/p&gt;

&lt;p&gt;That prevents a forwarded invitation link from automatically granting access to the wrong account.&lt;/p&gt;

&lt;p&gt;The invitations in this implementation expire after seven days.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Important Part: RLS
&lt;/h1&gt;

&lt;p&gt;Consider a &lt;code&gt;projects&lt;/code&gt; table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;projects

id
organization_id
name
description
created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every project belongs to an organization.&lt;/p&gt;

&lt;p&gt;Now imagine a user belonging to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organization_id = abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user should only be able to access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;projects.organization_id = abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and nothing belonging to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organization_id = xyz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The RLS policy expresses this relationship at the database level.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Current User
     │
     ▼
Is member of organization?
     │
   ┌─┴─┐
  YES  NO
   │    │
   ▼    ▼
 ALLOW DENY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is fundamentally different from simply hiding records in the frontend.&lt;/p&gt;




&lt;h1&gt;
  
  
  Frontend Checks Are Not Security
&lt;/h1&gt;

&lt;p&gt;This is worth emphasizing.&lt;/p&gt;

&lt;p&gt;This:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DeleteButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is useful.&lt;/p&gt;

&lt;p&gt;But it isn't security.&lt;/p&gt;

&lt;p&gt;A user can bypass the interface and send a request directly.&lt;/p&gt;

&lt;p&gt;That's why I prefer multiple layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────┐
│        UI checks         │
├──────────────────────────┤
│   Server authorization   │
├──────────────────────────┤
│ PostgreSQL RLS policies  │
└──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI controls the experience.&lt;/p&gt;

&lt;p&gt;Server-side authorization controls the operation.&lt;/p&gt;

&lt;p&gt;RLS protects the data.&lt;/p&gt;

&lt;p&gt;Each layer has a different responsibility.&lt;/p&gt;




&lt;h1&gt;
  
  
  Avoiding Recursive RLS Policies
&lt;/h1&gt;

&lt;p&gt;One problem I encountered was recursive policy evaluation.&lt;/p&gt;

&lt;p&gt;Suppose a project policy checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is the user a member of this organization?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That requires reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organization_members
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if &lt;code&gt;organization_members&lt;/code&gt; also has RLS policies, the database can end up evaluating policies that depend on other policies.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;projects
   ↓
organization_members
   ↓
RLS
   ↓
organization_members
   ↓
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To avoid this, the architecture uses carefully controlled &lt;code&gt;SECURITY DEFINER&lt;/code&gt; helper functions for membership checks.&lt;/p&gt;

&lt;p&gt;The result is a reusable authorization primitive that can be referenced by multiple RLS policies without creating recursive policy evaluation.&lt;/p&gt;




&lt;h1&gt;
  
  
  Next.js Authentication
&lt;/h1&gt;

&lt;p&gt;The application uses Supabase Auth with Next.js server-side authentication.&lt;/p&gt;

&lt;p&gt;The important part is keeping authentication state available correctly across server and browser contexts.&lt;/p&gt;

&lt;p&gt;The flow is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   │
   ▼
Supabase Auth
   │
   ▼
Session Cookie
   │
   ▼
Next.js Server
   │
   ▼
Authenticated User
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protected routes can then determine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Who is the user?
        ↓
Which organizations do they belong to?
        ↓
Which organization are they currently accessing?
        ↓
What can they do?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the organization context explicit rather than relying on client-side state alone.&lt;/p&gt;




&lt;h1&gt;
  
  
  Tenant-Scoped Projects
&lt;/h1&gt;

&lt;p&gt;Projects are used as the example tenant resource.&lt;/p&gt;

&lt;p&gt;A project belongs to exactly one organization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acme Inc.
 ├── Website Redesign
 ├── Mobile Application
 └── Marketing Platform

Startup Labs
 ├── AI Assistant
 └── Analytics Dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the current organization changes, the project dataset changes accordingly.&lt;/p&gt;

&lt;p&gt;The application doesn't need a separate codebase for each tenant.&lt;/p&gt;

&lt;p&gt;The same application handles all organizations.&lt;/p&gt;




&lt;h1&gt;
  
  
  Organization Switching
&lt;/h1&gt;

&lt;p&gt;The organization selector is another important part of the UX.&lt;/p&gt;

&lt;p&gt;A user may belong to several organizations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────┐
│ Acme Inc.          ▼ │
└──────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opening it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acme Inc.
Startup Labs
Example Corp.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selecting an organization changes the tenant context.&lt;/p&gt;

&lt;p&gt;The application then loads resources belonging to that organization.&lt;/p&gt;

&lt;p&gt;The database still enforces the final access boundary.&lt;/p&gt;




&lt;h1&gt;
  
  
  Application Structure
&lt;/h1&gt;

&lt;p&gt;The resulting route structure is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/
├── login
├── signup
├── dashboard
│
├── org/[orgId]
│   ├── members
│   ├── projects
│   └── invites
│
├── invite/[token]
│
└── auth/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a clear distinction between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public pages&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Organization context&lt;/li&gt;
&lt;li&gt;Tenant resources&lt;/li&gt;
&lt;li&gt;Invitation flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also leaves plenty of room for additional modules.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I Chose Supabase
&lt;/h1&gt;

&lt;p&gt;The goal wasn't to assemble dozens of services.&lt;/p&gt;

&lt;p&gt;Supabase gives the project several important pieces in one ecosystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Supabase
 ├── Authentication
 ├── PostgreSQL
 └── Row-Level Security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That combination is particularly useful for multi-tenant applications because authentication and database authorization can work together.&lt;/p&gt;

&lt;p&gt;Instead of building a separate authentication system and then trying to synchronize it with a separate database authorization layer, the application can use the authenticated identity directly in PostgreSQL security policies.&lt;/p&gt;




&lt;h1&gt;
  
  
  What This Architecture Doesn't Solve
&lt;/h1&gt;

&lt;p&gt;This isn't a complete SaaS business.&lt;/p&gt;

&lt;p&gt;There are deliberately no:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe subscriptions&lt;/li&gt;
&lt;li&gt;OAuth providers&lt;/li&gt;
&lt;li&gt;File storage&lt;/li&gt;
&lt;li&gt;Background jobs&lt;/li&gt;
&lt;li&gt;Audit logs&lt;/li&gt;
&lt;li&gt;Usage-based billing&lt;/li&gt;
&lt;li&gt;Automated test suite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those features depend heavily on the actual product being built.&lt;/p&gt;

&lt;p&gt;A CRM may need billing and audit logs.&lt;/p&gt;

&lt;p&gt;An AI application may need usage limits and model providers.&lt;/p&gt;

&lt;p&gt;A project management platform may need file storage and background processing.&lt;/p&gt;

&lt;p&gt;The foundation should stay generic.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Takeaway
&lt;/h1&gt;

&lt;p&gt;The biggest lesson from building this wasn't a specific Supabase feature.&lt;/p&gt;

&lt;p&gt;It was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenancy should be treated as an architectural boundary, not simply a database column.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A scalable model starts with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
 ↓
Membership
 ↓
Organization
 ↓
Tenant Resource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then security is enforced at multiple levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI
 ↓
Server Authorization
 ↓
PostgreSQL RLS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a much stronger foundation for building B2B SaaS applications.&lt;/p&gt;

&lt;p&gt;Instead of repeatedly solving authentication, organizations, roles, invitations, and tenant isolation, you can start with those pieces already established and focus on the actual product.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I packaged this architecture into a reusable &lt;strong&gt;Next.js + Supabase Multi-Tenant SaaS Starter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Supabase Auth&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Row-Level Security&lt;/li&gt;
&lt;li&gt;Organizations&lt;/li&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;Invitations&lt;/li&gt;
&lt;li&gt;Tenant-scoped Projects CRUD&lt;/li&gt;
&lt;li&gt;Member management&lt;/li&gt;
&lt;li&gt;DDEV support&lt;/li&gt;
&lt;li&gt;Vercel deployment support&lt;/li&gt;
&lt;li&gt;Database migrations&lt;/li&gt;
&lt;li&gt;Documentation&lt;/li&gt;
&lt;li&gt;Static demo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can explore the working demo here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://demo-supabase-multi-tenant-saa-s-st.vercel.app/" rel="noopener noreferrer"&gt;https://demo-supabase-multi-tenant-saa-s-st.vercel.app/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The article above is intentionally focused on the architecture rather than the product. If you are building a multi-tenant SaaS, the important part is understanding the security model and tenant boundaries before the application grows around them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fugo0dl80yj30rouldnhw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fugo0dl80yj30rouldnhw.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqaiedjroqf2sbxcqk50x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqaiedjroqf2sbxcqk50x.png" alt=" " width="799" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h3d773lrwax0qncwgcl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h3d773lrwax0qncwgcl.png" alt=" " width="800" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lnm39s66ybe1vbxqrjt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lnm39s66ybe1vbxqrjt.png" alt=" " width="799" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ku6n1a3xuc20zz3mrmh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ku6n1a3xuc20zz3mrmh.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63xvb2wi0peigxjr5so1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63xvb2wi0peigxjr5so1.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1i8mzwndtegi88nrz34m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1i8mzwndtegi88nrz34m.png" alt=" " width="799" height="379"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3yiejr6esrkcba6sjhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq3yiejr6esrkcba6sjhr.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqbosck7wyv6p7a39t985.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqbosck7wyv6p7a39t985.png" alt=" " width="799" height="382"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfa9jok5ju2s5dn1nlr0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyfa9jok5ju2s5dn1nlr0.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>nextjs</category>
      <category>saas</category>
      <category>security</category>
    </item>
    <item>
      <title>The 3 Lines of Stripe Webhook Code That Will Eventually Cost You Money</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Fri, 18 Sep 2026 12:54:13 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/the-3-lines-of-stripe-webhook-code-that-will-eventually-cost-you-money-12f0</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/the-3-lines-of-stripe-webhook-code-that-will-eventually-cost-you-money-12f0</guid>
      <description>&lt;p&gt;Stripe Webhooks in Laravel: 3 Production Failure Modes You Need to Handle&lt;/p&gt;

&lt;p&gt;Your Stripe integration works.&lt;/p&gt;

&lt;p&gt;You tested it with stripe trigger. The subscription appeared in the database. The happy path works.&lt;/p&gt;

&lt;p&gt;So you move on to the next feature.&lt;/p&gt;

&lt;p&gt;Then a customer emails you:&lt;/p&gt;

&lt;p&gt;“I was charged twice.”&lt;/p&gt;

&lt;p&gt;Or worse, nobody emails you.&lt;/p&gt;

&lt;p&gt;Stripe stopped reaching your webhook endpoint three weeks ago, and you only discover it when your MRR chart starts looking wrong.&lt;/p&gt;

&lt;p&gt;I spent the last few weeks building a production-grade Stripe webhook layer for Laravel 11 + Cashier. It has 40 tests and is now packaged as a standalone kit.&lt;/p&gt;

&lt;p&gt;This article covers the three failure modes that repeatedly show up in real-world Stripe integrations—and the implementation patterns that prevent them.&lt;/p&gt;

&lt;p&gt;The finished kit: &lt;a href="https://boukataya.gumroad.com/l/nggzho" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/nggzho&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The lessons below are free, whether you buy the kit or build everything yourself.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Stripe Retries Webhooks — So Your Handler Must Be Idempotent&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the failure mode everyone knows about.&lt;/p&gt;

&lt;p&gt;And one of the easiest to implement incorrectly.&lt;/p&gt;

&lt;p&gt;Stripe retries webhook deliveries when your endpoint doesn't return a 2xx response. Retries can continue for up to three days.&lt;/p&gt;

&lt;p&gt;The dangerous part is that your first request may still be running when Stripe sends the retry.&lt;/p&gt;

&lt;p&gt;For example, imagine your webhook handler does this:&lt;/p&gt;

&lt;p&gt;Receives invoice.paid&lt;br&gt;
Calls the Stripe API&lt;br&gt;
Sends a welcome email&lt;br&gt;
Updates several database records&lt;br&gt;
Finally returns 200&lt;/p&gt;

&lt;p&gt;If that takes too long, Stripe can retry while the first request is still executing.&lt;/p&gt;

&lt;p&gt;Now you have two workers processing the same event.&lt;/p&gt;

&lt;p&gt;The Idempotency Check That Looks Correct&lt;/p&gt;

&lt;p&gt;A common implementation is:&lt;/p&gt;

&lt;p&gt;if (WebhookEvent::where('stripe_event_id', $event-&amp;gt;id)-&amp;gt;exists()) {&lt;br&gt;
    return response()-&amp;gt;json(['received' =&amp;gt; true]);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;WebhookEvent::create([...]);&lt;/p&gt;

&lt;p&gt;Looks reasonable.&lt;/p&gt;

&lt;p&gt;It isn't safe.&lt;/p&gt;

&lt;p&gt;The Race Condition&lt;/p&gt;

&lt;p&gt;Two requests can execute the check at almost exactly the same time:&lt;/p&gt;

&lt;p&gt;Worker A:  SELECT ... → no row found&lt;br&gt;
Worker B:  SELECT ... → no row found&lt;br&gt;
                         ↑ both pass the check&lt;/p&gt;

&lt;p&gt;Worker A:  INSERT → succeeds&lt;br&gt;
Worker B:  INSERT → duplicate key error&lt;br&gt;
                         ↓&lt;br&gt;
                       HTTP 500&lt;br&gt;
                         ↓&lt;br&gt;
                    Stripe retries&lt;/p&gt;

&lt;p&gt;This is a classic time-of-check-to-time-of-use race condition.&lt;/p&gt;

&lt;p&gt;The application-level exists() check cannot close that window.&lt;/p&gt;

&lt;p&gt;And webhooks are exactly the type of traffic that exposes small concurrency windows: retries, bursts, and overlapping requests.&lt;/p&gt;

&lt;p&gt;Put the Guarantee in the Database&lt;/p&gt;

&lt;p&gt;The real protection belongs at the database level:&lt;/p&gt;

&lt;p&gt;$table-&amp;gt;string('stripe_event_id', 191)-&amp;gt;unique();&lt;/p&gt;

&lt;p&gt;Now the database guarantees that the same Stripe event cannot be inserted twice.&lt;/p&gt;

&lt;p&gt;When a concurrent request hits the unique constraint, catch the duplicate-key exception and treat it as a successful duplicate delivery.&lt;/p&gt;

&lt;p&gt;The important distinction is:&lt;/p&gt;

&lt;p&gt;A duplicate webhook is not a failure. It is an already-processed event.&lt;/p&gt;

&lt;p&gt;Your responsibility is to make sure the resulting state change happens once.&lt;/p&gt;

&lt;p&gt;Handle Multiple Database Engines&lt;/p&gt;

&lt;p&gt;If you develop with SQLite but deploy to MySQL or PostgreSQL, don't hard-code a single error code.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;private function isUniqueViolation(QueryException $e): bool&lt;br&gt;
{&lt;br&gt;
    $sqlState = $e-&amp;gt;errorInfo[0] ?? null;&lt;br&gt;
    $driverCode = $e-&amp;gt;errorInfo[1] ?? null;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// MySQL 23000/1062, PostgreSQL 23505, SQLite 23000/19.
return $sqlState === '23000'
    || $sqlState === '23505'
    || $driverCode === 1062
    || str_contains(
        $e-&amp;gt;getMessage(),
        'UNIQUE constraint failed'
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
Test the Race, Not Just the Happy Path&lt;/p&gt;

&lt;p&gt;A normal unit test won't prove that your implementation is concurrency-safe.&lt;/p&gt;

&lt;p&gt;You need to simulate the race itself.&lt;/p&gt;

&lt;p&gt;One approach is to use DB::listen() in Pest, inject a conflicting insert during the controller's own insert, and assert that the webhook still returns 200 with something like:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
    "duplicate": true&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;rather than returning 500.&lt;/p&gt;

&lt;p&gt;That's exactly the kind of test that catches bugs hidden behind otherwise-green test suites.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don't Do Real Work Inside the Webhook Request&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second problem is closely related to the first.&lt;/p&gt;

&lt;p&gt;Stripe has a timeout.&lt;/p&gt;

&lt;p&gt;Your application does not control how long Stripe is willing to wait.&lt;/p&gt;

&lt;p&gt;If your webhook handler spends too much time processing the event, Stripe can give up and retry.&lt;/p&gt;

&lt;p&gt;Now you have overlapping executions again.&lt;/p&gt;

&lt;p&gt;The Webhook Should Be Boring&lt;/p&gt;

&lt;p&gt;The HTTP endpoint should do roughly four things:&lt;/p&gt;

&lt;p&gt;Verify the signature&lt;br&gt;
Store the event&lt;br&gt;
Dispatch a job&lt;br&gt;
Return 200&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;// Signature verified above&lt;/p&gt;

&lt;p&gt;$record = WebhookEvent::create([...]);&lt;/p&gt;

&lt;p&gt;ProcessStripeWebhook::dispatch(&lt;br&gt;
    $record-&amp;gt;getKey(),&lt;br&gt;
    $eventId,&lt;br&gt;
    $eventType,&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;return response()-&amp;gt;json(['received' =&amp;gt; true]);&lt;/p&gt;

&lt;p&gt;Everything expensive happens asynchronously.&lt;/p&gt;

&lt;p&gt;Queue IDs, Not the Entire Payload&lt;/p&gt;

&lt;p&gt;There is another detail that is easy to overlook.&lt;/p&gt;

&lt;p&gt;Pass identifiers to the queue—not the entire Stripe payload.&lt;/p&gt;

&lt;p&gt;ProcessStripeWebhook::dispatch(&lt;br&gt;
    $record-&amp;gt;getKey(),&lt;br&gt;
    $eventId,&lt;br&gt;
    $eventType,&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;A large invoice.paid event containing dozens of line items can be hundreds of kilobytes.&lt;/p&gt;

&lt;p&gt;If Stripe retries the request three times and you serialize that entire payload into your queue on every attempt, you're unnecessarily multiplying your Redis memory usage.&lt;/p&gt;

&lt;p&gt;The event payload is already stored durably in your database.&lt;/p&gt;

&lt;p&gt;The queue only needs the IDs required to retrieve it.&lt;/p&gt;

&lt;p&gt;Configure Your Retry Policy&lt;/p&gt;

&lt;p&gt;The default queue configuration is rarely ideal for payment webhooks.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;public int $tries = 3;&lt;/p&gt;

&lt;p&gt;public int $timeout = 60;&lt;/p&gt;

&lt;p&gt;public function backoff(): array&lt;br&gt;
{&lt;br&gt;
    return [10, 30, 60];&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;This gives failed processing attempts some breathing room without retrying aggressively.&lt;/p&gt;

&lt;p&gt;But retries aren't enough.&lt;/p&gt;

&lt;p&gt;You also need a proper failure strategy.&lt;/p&gt;

&lt;p&gt;Failed Events Must Remain Replayable&lt;/p&gt;

&lt;p&gt;This is the subtle part.&lt;/p&gt;

&lt;p&gt;When the job fails permanently, do not mark the webhook as processed.&lt;/p&gt;

&lt;p&gt;public function failed(?Throwable $exception): void&lt;br&gt;
{&lt;br&gt;
    Log::error('Stripe webhook job failed permanently.', [&lt;br&gt;
        'stripe_event_id' =&amp;gt; $this-&amp;gt;stripeEventId,&lt;br&gt;
        'error' =&amp;gt; $exception?-&amp;gt;getMessage(),&lt;br&gt;
    ]);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Deliberately NOT marking processed_at.
// The event remains replayable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Imagine this sequence:&lt;/p&gt;

&lt;p&gt;Stripe event received&lt;br&gt;
        ↓&lt;br&gt;
Event stored&lt;br&gt;
        ↓&lt;br&gt;
Job dispatched&lt;br&gt;
        ↓&lt;br&gt;
Redis/API/database problem&lt;br&gt;
        ↓&lt;br&gt;
Job fails permanently&lt;br&gt;
        ↓&lt;br&gt;
Event marked "processed"&lt;/p&gt;

&lt;p&gt;You've now created a much worse problem.&lt;/p&gt;

&lt;p&gt;A temporary infrastructure failure has effectively deleted a payment event.&lt;/p&gt;

&lt;p&gt;Keep failed events replayable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Failed Payment Shouldn't Immediately Lock Your Customer Out&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one has a direct impact on customer experience.&lt;/p&gt;

&lt;p&gt;A card gets declined.&lt;/p&gt;

&lt;p&gt;Your webhook receives the failed payment event.&lt;/p&gt;

&lt;p&gt;Your application immediately revokes access.&lt;/p&gt;

&lt;p&gt;The customer was planning to update their card tomorrow.&lt;/p&gt;

&lt;p&gt;Now they can't access their account.&lt;/p&gt;

&lt;p&gt;Meanwhile, Stripe's Smart Retries may still be attempting to recover the payment.&lt;/p&gt;

&lt;p&gt;Your application has effectively given up before Stripe has.&lt;/p&gt;

&lt;p&gt;Give Failed Payments a Grace Period&lt;/p&gt;

&lt;p&gt;Instead of immediately terminating access, move the subscription into a recoverable state:&lt;/p&gt;

&lt;p&gt;$subscription-&amp;gt;forceFill([&lt;br&gt;
    'stripe_status' =&amp;gt; 'past_due',&lt;br&gt;
    'ends_at' =&amp;gt; $subscription-&amp;gt;ends_at&lt;br&gt;
        ?? now()-&amp;gt;addDays($graceDays),&lt;br&gt;
])-&amp;gt;save();&lt;/p&gt;

&lt;p&gt;With Laravel Cashier, ends_at determines when access stops.&lt;/p&gt;

&lt;p&gt;By pushing that date into the future, your application can continue treating the customer as subscribed while Stripe continues its payment recovery process.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Payment fails&lt;br&gt;
     ↓&lt;br&gt;
past_due&lt;br&gt;
     ↓&lt;br&gt;
7-day grace period&lt;br&gt;
     ↓&lt;br&gt;
Stripe retries payment&lt;br&gt;
     ↓&lt;br&gt;
Payment succeeds&lt;br&gt;
     ↓&lt;br&gt;
Subscription restored&lt;br&gt;
The Part People Forget: Reversal&lt;/p&gt;

&lt;p&gt;When payment succeeds again, remove the grace-period state:&lt;/p&gt;

&lt;p&gt;// on invoice.paid&lt;/p&gt;

&lt;p&gt;$subscription-&amp;gt;forceFill([&lt;br&gt;
    'stripe_status' =&amp;gt; 'active',&lt;br&gt;
    'ends_at' =&amp;gt; null,&lt;br&gt;
    'trial_ends_at' =&amp;gt; null,&lt;br&gt;
])-&amp;gt;save();&lt;/p&gt;

&lt;p&gt;Clearing ends_at matters.&lt;/p&gt;

&lt;p&gt;Otherwise, a customer who successfully recovered their payment can remain flagged as cancelling.&lt;/p&gt;

&lt;p&gt;Your UI might say:&lt;/p&gt;

&lt;p&gt;“Your subscription is ending.”&lt;/p&gt;

&lt;p&gt;Your dunning emails might say:&lt;/p&gt;

&lt;p&gt;“Your subscription will expire soon.”&lt;/p&gt;

&lt;p&gt;Even though the customer already paid.&lt;/p&gt;

&lt;p&gt;That's a state-management bug.&lt;/p&gt;

&lt;p&gt;Two More Details That Are Easy to Get Wrong&lt;/p&gt;

&lt;p&gt;The three issues above are the big ones.&lt;/p&gt;

&lt;p&gt;But there are two smaller implementation details worth getting right.&lt;/p&gt;

&lt;p&gt;Verify the Signature Against the Raw Request Body&lt;/p&gt;

&lt;p&gt;Stripe signs the exact bytes it sends.&lt;/p&gt;

&lt;p&gt;Don't decode and re-encode the JSON before verification.&lt;/p&gt;

&lt;p&gt;Correct&lt;br&gt;
$payload = $request-&amp;gt;getContent();&lt;br&gt;
Broken&lt;br&gt;
$payload = json_encode($request-&amp;gt;all());&lt;/p&gt;

&lt;p&gt;Those are not guaranteed to produce identical bytes.&lt;/p&gt;

&lt;p&gt;The safe sequence is:&lt;/p&gt;

&lt;p&gt;Raw request body&lt;br&gt;
       ↓&lt;br&gt;
Signature verification&lt;br&gt;
       ↓&lt;br&gt;
JSON decoding&lt;br&gt;
       ↓&lt;br&gt;
Event processing&lt;/p&gt;

&lt;p&gt;Not:&lt;/p&gt;

&lt;p&gt;Request&lt;br&gt;
   ↓&lt;br&gt;
Decode JSON&lt;br&gt;
   ↓&lt;br&gt;
Re-encode JSON&lt;br&gt;
   ↓&lt;br&gt;
Verify signature&lt;/p&gt;

&lt;p&gt;Nothing upstream should modify the request body before verification.&lt;/p&gt;

&lt;p&gt;In Laravel 11, keeping the webhook route in routes/api.php also avoids unnecessary session/CSRF middleware around the endpoint.&lt;/p&gt;

&lt;p&gt;Enforce the Timestamp Tolerance&lt;/p&gt;

&lt;p&gt;A valid signature from last week is still a replayable request.&lt;/p&gt;

&lt;p&gt;Signature verification should therefore include an appropriate timestamp tolerance.&lt;/p&gt;

&lt;p&gt;Fail Closed When the Webhook Secret Is Missing&lt;/p&gt;

&lt;p&gt;Never do this:&lt;/p&gt;

&lt;p&gt;if (!$secret) {&lt;br&gt;
    // Skip verification&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That turns a configuration mistake into a security vulnerability.&lt;/p&gt;

&lt;p&gt;If STRIPE_WEBHOOK_SECRET is missing, reject the request.&lt;/p&gt;

&lt;p&gt;Make the problem obvious:&lt;/p&gt;

&lt;p&gt;[stripe-kit] Stripe configuration incomplete:&lt;br&gt;
STRIPE_WEBHOOK_SECRET not set.&lt;/p&gt;

&lt;p&gt;The webhook endpoint will reject every request&lt;br&gt;
until this is configured.&lt;/p&gt;

&lt;p&gt;A loud failure in production is much better than silently accepting unsigned payment events.&lt;/p&gt;

&lt;p&gt;What I Actually Built&lt;/p&gt;

&lt;p&gt;I packaged these patterns into a drop-in Laravel + Cashier Stripe webhook kit.&lt;/p&gt;

&lt;p&gt;Not a course.&lt;/p&gt;

&lt;p&gt;Not a complete SaaS boilerplate.&lt;/p&gt;

&lt;p&gt;Just the parts that tend to break.&lt;/p&gt;

&lt;p&gt;Test Coverage&lt;/p&gt;

&lt;p&gt;The current test suite:&lt;/p&gt;

&lt;p&gt;40 tests passed&lt;br&gt;
106 assertions&lt;/p&gt;

&lt;p&gt;Duration: 2.49s&lt;/p&gt;

&lt;p&gt;Tested against:&lt;/p&gt;

&lt;p&gt;Laravel 11.56&lt;br&gt;
Cashier 15.8&lt;br&gt;
Stripe PHP SDK 16.6&lt;br&gt;
Pest 3.8&lt;/p&gt;

&lt;p&gt;The tests run against MySQL rather than SQLite because the unique-index and concurrency behaviour are central to the implementation.&lt;/p&gt;

&lt;p&gt;Testing only on SQLite could give you a false sense of confidence.&lt;/p&gt;

&lt;p&gt;What's Included&lt;br&gt;
Raw-body Stripe signature verification with configurable tolerance&lt;br&gt;
Database-enforced webhook idempotency&lt;br&gt;
Concurrency-safe duplicate handling&lt;br&gt;
Queue-based processing&lt;br&gt;
Three attempts with exponential backoff&lt;br&gt;
Replayable failed events&lt;br&gt;
Subscription synchronization for seven relevant Stripe event types&lt;br&gt;
Failed-payment grace periods&lt;br&gt;
Correct grace-period reversal after successful payment&lt;br&gt;
php artisan stripe:status for pipeline health checks&lt;br&gt;
php artisan stripe:replay for unfinished events&lt;br&gt;
DDEV environment with PHP, MySQL 8, Redis, Mailpit, and Stripe CLI&lt;br&gt;
Six documentation guides covering deployment and troubleshooting&lt;br&gt;
Supervisor and systemd deployment configurations&lt;br&gt;
Symptom → cause → fix troubleshooting reference&lt;/p&gt;

&lt;p&gt;Get the kit: &lt;a href="https://boukataya.gumroad.com/l/nggzho" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/nggzho&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What It Doesn't Include&lt;/p&gt;

&lt;p&gt;I'd rather make this explicit before you buy it.&lt;/p&gt;

&lt;p&gt;No frontend&lt;/p&gt;

&lt;p&gt;There is no:&lt;/p&gt;

&lt;p&gt;Blade frontend&lt;br&gt;
Inertia&lt;br&gt;
Livewire&lt;br&gt;
SPA&lt;br&gt;
Checkout UI&lt;/p&gt;

&lt;p&gt;The part that creates subscriptions is yours.&lt;/p&gt;

&lt;p&gt;This kit handles what happens after Stripe starts sending events back to your application.&lt;/p&gt;

&lt;p&gt;No authentication scaffolding&lt;/p&gt;

&lt;p&gt;There is no auth system or admin panel.&lt;/p&gt;

&lt;p&gt;Use something like Breeze or Filament depending on your application.&lt;/p&gt;

&lt;p&gt;No Stripe account or products&lt;/p&gt;

&lt;p&gt;You still need to configure your own:&lt;/p&gt;

&lt;p&gt;Stripe account&lt;br&gt;
Products&lt;br&gt;
Prices&lt;br&gt;
Webhook endpoint&lt;br&gt;
API credentials&lt;br&gt;
No magic guarantee&lt;/p&gt;

&lt;p&gt;Stripe has edge cases.&lt;/p&gt;

&lt;p&gt;This isn't a promise that your billing system can never fail.&lt;/p&gt;

&lt;p&gt;It's a tested foundation built around Cashier's established patterns, with the failure modes that are easy to overlook handled explicitly.&lt;/p&gt;

&lt;p&gt;If You're Building This Yourself&lt;/p&gt;

&lt;p&gt;If you take nothing else from this article, take these three points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Put the uniqueness guarantee in the database
$table-&amp;gt;string('stripe_event_id', 191)-&amp;gt;unique();&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Application-level exists() checks are not enough.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Queue the work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your webhook should follow this flow:&lt;/p&gt;

&lt;p&gt;Verify&lt;br&gt;
  ↓&lt;br&gt;
Store&lt;br&gt;
  ↓&lt;br&gt;
Dispatch&lt;br&gt;
  ↓&lt;br&gt;
Return 200&lt;/p&gt;

&lt;p&gt;Don't perform expensive business logic inside the HTTP request.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don't revoke access on the first failed payment&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stripe may still be retrying.&lt;/p&gt;

&lt;p&gt;Give the customer a grace period and clear that grace period when the payment succeeds.&lt;/p&gt;

&lt;p&gt;The Bigger Lesson&lt;/p&gt;

&lt;p&gt;Stripe webhooks aren't difficult because the happy path is complicated.&lt;/p&gt;

&lt;p&gt;They're difficult because distributed systems don't care about your happy path.&lt;/p&gt;

&lt;p&gt;Requests overlap.&lt;/p&gt;

&lt;p&gt;Networks fail.&lt;/p&gt;

&lt;p&gt;Queues fail.&lt;/p&gt;

&lt;p&gt;Payments fail.&lt;/p&gt;

&lt;p&gt;Retries happen.&lt;/p&gt;

&lt;p&gt;And the database has to be the final source of truth when two workers believe they're processing the same event.&lt;/p&gt;

&lt;p&gt;That's where most of the bugs live.&lt;/p&gt;

&lt;p&gt;If you're building this yourself, hopefully this saves you a week of debugging.&lt;/p&gt;

&lt;p&gt;If you'd rather not rediscover these details at 3 AM:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://boukataya.gumroad.com/l/nggzho" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/nggzho&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you've built a Stripe integration yourself, the interesting question isn't whether it works.&lt;/p&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;p&gt;What happens when it doesn't?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzsd1bhindyzsrwz3ttg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkzsd1bhindyzsrwz3ttg.png" alt=" " width="799" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffeboa1gag7xur4yf41sr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffeboa1gag7xur4yf41sr.png" alt=" " width="799" height="259"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiec5hp9hndje205aae10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiec5hp9hndje205aae10.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6ahrze3s500n5c3zmuo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6ahrze3s500n5c3zmuo.png" alt=" " width="800" height="290"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5k8ldzzan88irsmby2ia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5k8ldzzan88irsmby2ia.png" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcnqap1wou5auknji0n1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcnqap1wou5auknji0n1.png" alt=" " width="799" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>laravel</category>
      <category>webhooks</category>
      <category>php</category>
    </item>
    <item>
      <title>I Built a Production-Ready Laravel SaaS Starter Kit</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Sat, 05 Sep 2026 11:12:42 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-production-ready-laravel-saas-starter-kit-5326</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-production-ready-laravel-saas-starter-kit-5326</guid>
      <description>&lt;p&gt;Starting a new SaaS project often means rebuilding the same foundation:&lt;/p&gt;

&lt;p&gt;Authentication&lt;br&gt;
Password reset&lt;br&gt;
Email verification&lt;br&gt;
Dashboard layouts&lt;br&gt;
User management&lt;br&gt;
Settings pages&lt;br&gt;
Database structure&lt;br&gt;
Docker configuration&lt;br&gt;
Email testing&lt;br&gt;
Authorization and validation&lt;br&gt;
That repetitive setup can take days before the actual product work begins.&lt;/p&gt;

&lt;p&gt;I built Laravel SaaS Starter Kit to provide a clean, extensible starting point for SaaS products, MVPs, internal tools, and client applications.&lt;/p&gt;

&lt;p&gt;What’s included?&lt;br&gt;
Laravel 13 and PHP 8.3+&lt;br&gt;
Registration, login, logout, and remember me&lt;br&gt;
Password reset and email verification&lt;br&gt;
Login throttling and secure password hashing&lt;br&gt;
Responsive Blade and Tailwind dashboard&lt;br&gt;
Database-backed dashboard statistics&lt;br&gt;
Admin-only user management&lt;br&gt;
Search and pagination&lt;br&gt;
User roles and authorization policies&lt;br&gt;
Profile, password, and account settings&lt;br&gt;
MySQL migrations and seeders&lt;br&gt;
Docker Compose support&lt;br&gt;
Mailpit for local email testing&lt;br&gt;
Vite and Tailwind CSS&lt;br&gt;
PHPUnit feature tests&lt;br&gt;
Production setup and deployment documentation&lt;br&gt;
Custom error pages and reusable Blade components&lt;br&gt;
The project includes realistic demo data so the dashboard feels useful immediately after seeding the database.&lt;/p&gt;

&lt;p&gt;Why I built it&lt;br&gt;
I wanted a Laravel foundation that felt closer to something a professional developer or small agency would actually use, rather than a basic tutorial project.&lt;/p&gt;

&lt;p&gt;The goal is to remove repetitive infrastructure work while keeping the code familiar and easy to customize. It uses Laravel-native patterns such as controllers, policies, Eloquent models, Blade components, migrations, factories, and feature tests.&lt;/p&gt;

&lt;p&gt;Built for&lt;br&gt;
Freelance developers&lt;br&gt;
SaaS founders&lt;br&gt;
Web agencies&lt;br&gt;
Full-stack developers&lt;br&gt;
Small development teams&lt;br&gt;
Internal tools and MVPs&lt;br&gt;
The starter kit is available as a downloadable product for developers who want to move from idea to working SaaS foundation faster.&lt;/p&gt;

&lt;p&gt;I’d love to hear what you usually build first when starting a new Laravel SaaS project. Which repetitive setup task takes the most time for you?&lt;/p&gt;

&lt;h1&gt;
  
  
  laravel #php #saas #opensource #webdev
&lt;/h1&gt;

&lt;p&gt;link :&lt;a href="https://boukataya.gumroad.com/l/zeovfd" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/zeovfd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ful9lxzeu36v0emp7wpfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ful9lxzeu36v0emp7wpfk.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvld5jrbxysdzgmf7q7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvld5jrbxysdzgmf7q7y.png" alt=" " width="799" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1ii9916d8ofkn3glhol.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh1ii9916d8ofkn3glhol.png" alt=" " width="800" height="427"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvg8njf9071z3e1p9bl3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbvg8njf9071z3e1p9bl3.png" alt=" " width="800" height="428"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdke6tve2d0uc4a8rtcp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbdke6tve2d0uc4a8rtcp.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjozvcbe3j0o7dcdtg0nc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjozvcbe3j0o7dcdtg0nc.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9nw3hy0iv8ecx2p6k0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9nw3hy0iv8ecx2p6k0m.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>saas</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build a Premium Developer Portfolio in Minutes with This Next.js Template</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Sat, 29 Aug 2026 23:51:00 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/build-a-premium-developer-portfolio-in-minutes-with-this-nextjs-template-4db9</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/build-a-premium-developer-portfolio-in-minutes-with-this-nextjs-template-4db9</guid>
      <description>&lt;p&gt;If you want a portfolio that looks premium, feels modern, and helps you land freelance work or job opportunities, this template is built for that.&lt;/p&gt;

&lt;p&gt;I created a polished Next.js portfolio template designed for developers, freelancers, and founders. It includes a clean landing page, project showcase, blog, resume section, contact page, and modern dark/light theming.&lt;/p&gt;

&lt;p&gt;Demo:&lt;br&gt;
&lt;a href="https://portex-blue.vercel.app/" rel="noopener noreferrer"&gt;https://portex-blue.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Product:&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/gdnxr" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/gdnxr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why I built it&lt;br&gt;
A lot of portfolio templates feel generic or outdated. I wanted something that feels more like a premium product site than a basic starter template.&lt;/p&gt;

&lt;p&gt;This template is built with:&lt;/p&gt;

&lt;p&gt;Next.js&lt;br&gt;
TypeScript&lt;br&gt;
Tailwind CSS&lt;br&gt;
Framer Motion&lt;br&gt;
Responsive layout&lt;br&gt;
Modern portfolio sections&lt;br&gt;
What’s included&lt;br&gt;
Hero section&lt;br&gt;
About page&lt;br&gt;
Projects showcase&lt;br&gt;
Blog section&lt;br&gt;
Resume page&lt;br&gt;
Contact form&lt;br&gt;
Theme switcher&lt;br&gt;
Clean and editable content structure&lt;br&gt;
Why this helps&lt;br&gt;
Instead of spending days building a portfolio from scratch, you can start with a strong, production-ready foundation and customize it to your brand.&lt;/p&gt;

&lt;p&gt;It’s especially useful if you want to:&lt;/p&gt;

&lt;p&gt;showcase your work&lt;br&gt;
attract freelance clients&lt;br&gt;
land product or engineering jobs&lt;br&gt;
present yourself as a polished developer&lt;br&gt;
Demo &amp;amp; product&lt;br&gt;
Live preview:&lt;br&gt;
&lt;a href="https://portex-blue.vercel.app/" rel="noopener noreferrer"&gt;https://portex-blue.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Buy the template:&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/gdnxr" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/gdnxr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want a portfolio that looks more premium and feels closer to a real product website, this is a great starting point.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>portfolio</category>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>Launch Your AI SaaS Website Without Starting From Scratch</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Wed, 19 Aug 2026 14:01:05 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/launch-your-ai-saas-website-without-starting-from-scratch-2c2</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/launch-your-ai-saas-website-without-starting-from-scratch-2c2</guid>
      <description>&lt;p&gt;Building an AI product is difficult enough. Your website should not be another six-week project.&lt;/p&gt;

&lt;p&gt;Many AI startups spend too much time assembling basic marketing pages: a hero section, feature grid, pricing table, FAQ, blog, contact page, responsive navigation, and SEO setup. Each part seems simple on its own, but together they create a large amount of design and development work.&lt;/p&gt;

&lt;p&gt;That is why I built Lumen, a premium AI SaaS landing page template for founders, developers, and small teams.&lt;/p&gt;

&lt;p&gt;A Complete Starting Point for AI Products&lt;br&gt;
Lumen is designed for AI SaaS products, developer tools, productivity apps, internal knowledge platforms, and technical startups.&lt;/p&gt;

&lt;p&gt;Instead of beginning with an empty project, you get a complete marketing website with:&lt;/p&gt;

&lt;p&gt;A conversion-focused homepage&lt;br&gt;
Product mockups and feature sections&lt;br&gt;
Use cases and customer testimonials&lt;br&gt;
Pricing with monthly and yearly billing options&lt;br&gt;
FAQ accordion&lt;br&gt;
Blog index and article pages&lt;br&gt;
About and contact pages&lt;br&gt;
Privacy policy and terms pages&lt;br&gt;
Custom 404 page&lt;br&gt;
Responsive mobile navigation&lt;br&gt;
Light and dark themes&lt;br&gt;
The demo content presents an AI document intelligence product that connects company knowledge sources and returns answers with citations. Every section is structured so you can replace the copy with your own product details.&lt;/p&gt;

&lt;p&gt;Built for Modern Development&lt;br&gt;
Lumen uses a straightforward, maintainable stack:&lt;/p&gt;

&lt;p&gt;Next.js 16&lt;br&gt;
React 19&lt;br&gt;
TypeScript&lt;br&gt;
Tailwind CSS v4&lt;br&gt;
Lucide icons&lt;br&gt;
next-themes&lt;br&gt;
The project uses the Next.js App Router and follows a clean component-based structure. Site content is separated into typed files, making it easy to update your brand, navigation, pricing, features, testimonials, FAQ entries, and blog posts.&lt;/p&gt;

&lt;p&gt;You do not need to hunt through every component to change basic content.&lt;/p&gt;

&lt;p&gt;Designed to Be Customized&lt;br&gt;
The template includes a central site configuration file for the most important brand details:&lt;/p&gt;

&lt;p&gt;Product name&lt;br&gt;
Tagline&lt;br&gt;
Description&lt;br&gt;
Website URL&lt;br&gt;
Contact email&lt;br&gt;
Social links&lt;br&gt;
Call-to-action labels&lt;br&gt;
Announcement banner&lt;br&gt;
Homepage statistics&lt;br&gt;
The visual system is also easy to adapt. Update the theme colors and font configuration to create a brand identity that feels specific to your product rather than looking like a generic starter template.&lt;/p&gt;

&lt;p&gt;Ready for Real Product Integrations&lt;br&gt;
Lumen is a frontend marketing template, but it is prepared for the services most products need.&lt;/p&gt;

&lt;p&gt;You can connect:&lt;/p&gt;

&lt;p&gt;Stripe payment links or checkout&lt;br&gt;
Formspree, Resend, or your own form endpoint&lt;br&gt;
Authentication providers&lt;br&gt;
Analytics platforms&lt;br&gt;
Your own API routes&lt;br&gt;
A custom backend&lt;br&gt;
The template does not force you into a particular SaaS architecture. It gives you the presentation layer, structure, and visual foundation while leaving the product-specific integrations up to you.&lt;/p&gt;

&lt;p&gt;SEO and Deployment Included&lt;br&gt;
The project includes essential SEO infrastructure out of the box:&lt;/p&gt;

&lt;p&gt;Page-specific metadata&lt;br&gt;
Sitemap generation&lt;br&gt;
Robots configuration&lt;br&gt;
Open Graph image generation&lt;br&gt;
Static blog routes&lt;br&gt;
Semantic HTML&lt;br&gt;
Accessible focus states&lt;br&gt;
Keyboard-friendly FAQ interactions&lt;br&gt;
Once your content is customized, the project can be deployed to Vercel with the standard Next.js workflow:&lt;/p&gt;

&lt;p&gt;Who Is Lumen For?&lt;br&gt;
Lumen is a good fit for:&lt;/p&gt;

&lt;p&gt;AI startup founders&lt;br&gt;
Indie hackers&lt;br&gt;
SaaS developers&lt;br&gt;
Agencies building AI product sites&lt;br&gt;
Product designers creating a fast MVP&lt;br&gt;
Teams launching a developer tool&lt;br&gt;
Anyone who needs a professional SaaS website quickly&lt;br&gt;
It is especially useful when the product is still evolving and you need a strong public-facing website before building every marketing feature from scratch.&lt;/p&gt;

&lt;p&gt;Start With the Product, Not the Boilerplate&lt;br&gt;
A landing page should communicate what your product does, who it helps, and why people should trust it.&lt;/p&gt;

&lt;p&gt;Lumen gives you those essential building blocks in one polished project, so you can spend more time refining your product and less time recreating common website patterns.&lt;/p&gt;

&lt;p&gt;Get the template on Gumroad:&lt;/p&gt;

&lt;p&gt;demo : &lt;/p&gt;
&lt;div class="ltag-netlify"&gt;
  &lt;iframe src="https://lumenv-2.netlify.app/" title="Netlify embed"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://boukataya.gumroad.com/l/xqdzjo" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2Fswc4n89hv7vvp64hnr5g13r1v4db" height="541" class="m-0" width="1005"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://boukataya.gumroad.com/l/xqdzjo" rel="noopener noreferrer" class="c-link"&gt;
            Lumen — AI SaaS Landing Page Template
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Lumen — AI SaaS Landing Page TemplateLaunch your AI product with a polished, conversion-focused website built with Next.js, React, TypeScript, and Tailwind CSS.Lumen gives you the complete marketing foundation for an AI SaaS, startup, or developer tool. Replace the demo content with your own brand, connect your forms and payments, and deploy to Vercel.Included Complete responsive homepage Hero section with product mockup Features and use cases sections How-it-works workflow Testimonials and logo cloud Pricing page with monthly/yearly toggle FAQ accordion Blog index and article pages About, contact, privacy, and terms pages Light and dark mode Mobile navigation SEO metadata, sitemap, robots.txt, and Open Graph image generation Accessible components and keyboard-friendly interactions Clean TypeScript architecture Zero-config Vercel deployment Built With Next.js 16 React 19 TypeScript Tailwind CSS v4 Lucide icons next-themes Important NotesThis is a frontend marketing template. Forms, authentication, analytics, and payment links are prepared for integration but are not connected to external services. No database is required for the landing page.Use Lumen to create a professional AI product website without starting from an empty project.demo : demo: https://lumenv-2.netlify.app/
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2F7ai3z4dx2bl2bs70di842sywirml" width="400" height="250"&gt;
          boukataya.gumroad.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko9g6g9ghon4qvuq82lr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko9g6g9ghon4qvuq82lr.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fargr7xnp0iu96ve9b2ld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fargr7xnp0iu96ve9b2ld.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv79htxf21fa0wh3wze8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv79htxf21fa0wh3wze8i.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa5dd659i82bp5gdlzc40.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa5dd659i82bp5gdlzc40.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>saas</category>
      <category>design</category>
    </item>
    <item>
      <title>Lumen: The AI SaaS Landing Page Template That Ships in Hours</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:59:35 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/lumen-the-ai-saas-landing-page-template-that-ships-in-hours-2n81</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/lumen-the-ai-saas-landing-page-template-that-ships-in-hours-2n81</guid>
      <description>&lt;p&gt;Launching an AI product is hard. Marketing it shouldn’t be.&lt;/p&gt;

&lt;p&gt;For indie hackers and startup founders building AI-powered tools, the biggest friction point isn’t always the product — it’s getting it in front of customers. A professional landing page matters more than ever in a crowded market. But building one from scratch? That’s hours of design tweaking, component wrangling, SEO setup, and deployment configuration.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Lumen&lt;/strong&gt;: a premium, dark-first landing page template designed specifically for AI products and SaaS startups. It’s the answer to founders who ask, “Can I launch a beautiful, conversion-focused marketing site this afternoon — not this weekend?”&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Lumen?
&lt;/h2&gt;

&lt;p&gt;Lumen is a production-ready landing page template built with modern web technologies. While the template itself showcases a document Q&amp;amp;A engine (turning PDFs and Notion pages into cited answers), Lumen is versatile enough to market any AI product: an LLM wrapper, a data analytics tool, a generative design platform, or a specialized AI agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Concept:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Turn messy documents into accurate answers in seconds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A source-backed Q&amp;amp;A engine for documents and knowledge bases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Plain English queries with every answer cited&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here’s the beauty: the template is completely customizable. Change the copy, rebrand the colors, swap the use cases, and it works for your AI product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Foundation
&lt;/h2&gt;

&lt;p&gt;Built on a modern, no-bloat stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next.js App Router&lt;/strong&gt; — Server components, streaming, optimized builds&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TypeScript (strict mode)&lt;/strong&gt; — Type safety from day one&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; — Utility-first styling, minimal bundle size&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;next-themes&lt;/strong&gt; — Seamless light/dark mode switching&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lucide icons&lt;/strong&gt; — Beautiful, lightweight icon system&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The philosophy:&lt;/strong&gt; No UI library dependencies. No backend database. No environment variables. Deploy to Vercel with one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get
&lt;/h2&gt;

&lt;h3&gt;
  
  
  9 Complete Pages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Homepage&lt;/strong&gt; — 13 conversion-focused sections&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; — Monthly/yearly toggle, interactive comparisons&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blog&lt;/strong&gt; — Index page + individual post pages with related content&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;About&lt;/strong&gt; — Team and mission storytelling&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Contact&lt;/strong&gt; — Email capture form (ready to wire up to Formspree/Resend)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Privacy &amp;amp; Terms&lt;/strong&gt; — Legal pages with placeholder copy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom 404&lt;/strong&gt; — Branded error page&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  13 Homepage Sections
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Navbar&lt;/strong&gt; — Sticky header with light/dark toggle&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hero&lt;/strong&gt; — Promise + CTAs + proof stats + CSS-only product mockup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Logo Cloud&lt;/strong&gt; — Marquee of fake customer logos&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt; — Paint the pain point&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Features&lt;/strong&gt; — Highlight 3–4 key capabilities&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How It Works&lt;/strong&gt; — 3-step workflow visualization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Product Preview&lt;/strong&gt; — Full fake app screenshot&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Cases&lt;/strong&gt; — Real-world applications&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testimonials&lt;/strong&gt; — Social proof with quotes + avatars&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt; — Plans with feature matrices&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FAQ&lt;/strong&gt; — Accessible, animated accordion&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Final CTA&lt;/strong&gt; — Email capture or signup button&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Footer&lt;/strong&gt; — Links, socials, copyright&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Design System
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dark-first aesthetic&lt;/strong&gt; — Deep ink backgrounds, premium feel&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Warm accents&lt;/strong&gt; — Amber highlights + soft teal secondary&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Textures&lt;/strong&gt; — Subtle grain and grid overlays for depth&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Glass panels&lt;/strong&gt; — Semi-transparent surfaces with backdrop blur&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rounded cards&lt;/strong&gt; — &lt;code&gt;rounded-2xl&lt;/code&gt; for modern, friendly feel&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Micro-interactions&lt;/strong&gt; — Button shine sweep, card hover lift, animated accordions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;SEO Ready Out of the Box&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Per-page metadata with Open Graph&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sitemap.ts&lt;/code&gt; for search engine crawling&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;robots.txt&lt;/code&gt; for crawler directives&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Semantic HTML with clean heading hierarchy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Canonical URLs for duplicate prevention&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Accessibility First&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Keyboard navigation throughout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Visible focus states on all interactive elements&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ARIA-wired forms and navigation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reduced-motion support for animations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Screen reader friendly structure&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Light &amp;amp; Dark Mode&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Toggle in the navbar&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Persistent across sessions (via &lt;code&gt;next-themes&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatic OS preference detection&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No layout shift on theme change&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;All Copy is Centralized&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Want to rebrand Lumen for your AI product? Every marketing message lives in &lt;code&gt;/lib&lt;/code&gt; files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;site.ts&lt;/code&gt; — Brand name, tagline, email, CTAs, stats&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;nav.ts&lt;/code&gt; — Navigation and footer links&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;pricing.ts&lt;/code&gt; — Pricing plans and features&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;features.ts&lt;/code&gt; — Core feature descriptions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;testimonials.ts&lt;/code&gt; — Social proof quotes&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;faq.ts&lt;/code&gt; — Frequently asked questions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;blog.ts&lt;/code&gt; — Blog post metadata&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;legal.ts&lt;/code&gt; — Privacy and terms copy&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No digging through components. No hunting through JSX. One place to edit everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;strong&gt;Zero Configuration Deployment&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Push to GitHub&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import into Vercel&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One click deploy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No environment variables&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No database migrations&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTTPS by default&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Global CDN&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Customization: The 10-Minute Rebrand
&lt;/h2&gt;

&lt;p&gt;Want to make Lumen your own? Three changes:&lt;/p&gt;

&lt;p&gt;| What | Where |&lt;/p&gt;

&lt;p&gt;| — — | — — |&lt;/p&gt;

&lt;p&gt;| Brand name, tagline, stats, CTAs | &lt;code&gt;src/lib/site.ts&lt;/code&gt; |&lt;/p&gt;

&lt;p&gt;| Colors (amber/teal accents, ink backgrounds) | CSS variables in &lt;code&gt;src/app/globals.css&lt;/code&gt; |&lt;/p&gt;

&lt;p&gt;| Marketing copy | All &lt;code&gt;src/lib/*.ts&lt;/code&gt; files |&lt;/p&gt;

&lt;p&gt;Update these three places and your entire site rebrands itself — navbar, footer, metadata, everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built-in TODOs for Developers
&lt;/h3&gt;

&lt;p&gt;Search the codebase for &lt;code&gt;TODO&lt;/code&gt; to find integration points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Email capture&lt;/strong&gt; — Connect to Formspree, Resend, or your email service&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payment links&lt;/strong&gt; — Point pricing CTAs at Stripe checkout&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analytics&lt;/strong&gt; — Add Plausible, PostHog, or Google Analytics snippet&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; — Wire up your login/signup flow&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who Should Use Lumen?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ideal For:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI founders&lt;/strong&gt; launching their first product&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SaaS startups&lt;/strong&gt; in crowded markets (need to stand out fast)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Document processing tools&lt;/strong&gt; — Q&amp;amp;A, RAG, summarization&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI agents&lt;/strong&gt; — Specialized LLM wrappers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data intelligence platforms&lt;/strong&gt; — Analytics, insights, predictions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anyone who wants a professional site yesterday&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Not For:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Marketing teams needing highly custom design&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enterprise deployments with complex authentication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Products requiring a headless CMS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Teams wanting to build from scratch (template removes that option)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Competition
&lt;/h2&gt;

&lt;p&gt;Let’s be honest: landing page builders exist. Webflow, Framer, Wix. But they require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Monthly subscriptions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Slow design/deploy cycles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vendor lock-in&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Limited customization without designers&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lumen is different:&lt;/p&gt;

&lt;p&gt;Download the Medium App&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Own it.&lt;/strong&gt; It’s yours. Full source code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Fast.&lt;/strong&gt; Ship in hours, not days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Cheap.&lt;/strong&gt; One-time purchase, no recurring fees.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Flexible.&lt;/strong&gt; Change anything, deploy anywhere.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Modern.&lt;/strong&gt; Built with current best practices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Scalable.&lt;/strong&gt; Handles millions of visitors on Vercel’s edge network.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;AI-Powered Document Q&amp;amp;A&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A startup building a tool to turn company wikis into searchable AI assistants. Uses Lumen to launch with gorgeous product preview, feature breakdown, and pricing tiers.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Specialized LLM Wrapper&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;An indie developer created a domain-specific language model for medical research. Lumen’s hero section and testimonials sell the accuracy and speed. Blog section builds SEO credibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Data Analytics Tool&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A team built an AI-powered analytics dashboard. Lumen’s “How It Works” section explains the 3-step process, and the pricing section handles different team sizes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;AI Content Generator&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A founder’s side project for generating product copy. Lumen’s blog section drives organic traffic. Pricing tiers separate freemium from pro users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Next.js App Router&lt;/strong&gt; ensures fast client-side navigation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Server components&lt;/strong&gt; by default reduce JavaScript bundle&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; generates only used styles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image optimization&lt;/strong&gt; via &lt;code&gt;next/image&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Font optimization&lt;/strong&gt; via &lt;code&gt;next/font&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vercel Edge Functions&lt;/strong&gt; for global CDN delivery&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Core Web Vitals in the green zone out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Styling Architecture
&lt;/h3&gt;

&lt;p&gt;Lumen uses Tailwind CSS with custom design tokens defined in CSS variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;
&lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nt"&gt;color-accent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;hsl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nt"&gt;accent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;alpha-value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;);&lt;/span&gt;

&lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nt"&gt;color-accent-soft&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;hsl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;var&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nt"&gt;accent-soft&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;alpha-value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change a single variable, and the theme updates everywhere. No cascading issues. No specificity wars.&lt;/p&gt;

&lt;h3&gt;
  
  
  Component Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
components/

├── layout/ # Navbar, Footer, Container, Section

├── sections/ # 13 homepage sections

├── ui/ # Reusable: Button, Badge, Card, Accordion

└── blog/ # BlogCard, BlogContent

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each section is self-contained, making it easy to move, hide, or reorder sections on the homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost of Ownership
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upfront:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Template purchase (one-time)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recurring (minimal):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Vercel hosting (~$20/month, or free for small sites)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optional: Email service (Formspree, Resend, SendGrid)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optional: Analytics (Plausible, PostHog)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optional: Blog CMS if you want (headless CMS like Sanity, not required)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total Cost:&lt;/strong&gt; $0–$50/month depending on scale. Compare that to a custom agency build ($5K–$50K) or a Webflow subscription ($12/month + designer time).&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vercel (Recommended)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Push repo to GitHub&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import project in Vercel&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One click deploy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatic SSL, domain setup, branch previews&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Other Platforms
&lt;/h3&gt;

&lt;p&gt;Works anywhere Node.js runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AWS EC2 + ECS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DigitalOcean App Platform&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Railway&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Render&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Self-hosted VPS&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SEO &amp;amp; Marketing
&lt;/h2&gt;

&lt;p&gt;Lumen is built for organic discovery:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Semantic HTML&lt;/strong&gt; — Proper heading hierarchy, structured data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fast load times&lt;/strong&gt; — Top Core Web Vitals&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mobile responsive&lt;/strong&gt; — Works beautifully on all devices&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blog system&lt;/strong&gt; — Build authority with keyword-targeted posts&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open Graph meta tags&lt;/strong&gt; — Pretty social media previews&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sitemap &amp;amp; robots.txt&lt;/strong&gt; — Search engines index everything&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Canonical URLs&lt;/strong&gt; — Avoid duplicate content penalties&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For AI products especially, a strong marketing site + blog = SEO authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations &amp;amp; Honest Assessment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Lumen is NOT:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A CMS (blog posts are data files, not database records)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A backend as a service (you integrate with external services)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A drag-and-drop builder (requires code edits for major changes)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A design tool replacement (opinions baked into the template)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Best Practices:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Customize copy before launching (don’t use fake product names)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replace fake logos with real customer logos (or remove the section)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update testimonials with real reviews (or remove)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Wire up email capture forms before promoting them&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add analytics to track conversions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Installation (5 minutes)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="c"&gt;# Clone or download the template&lt;/span&gt;

&lt;span class="nb"&gt;cd &lt;/span&gt;lumen

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;

npm &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# Start dev server&lt;/span&gt;

npm run dev

&lt;span class="c"&gt;# Open http://localhost:3000&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customization (10 minutes)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Edit &lt;code&gt;src/lib/site.ts&lt;/code&gt; with your product name and tagline&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update colors in &lt;code&gt;src/app/globals.css&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Edit copy in &lt;code&gt;src/lib/*.ts&lt;/code&gt; files&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replace favicon and OG image&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deploy to Vercel&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Ready for Launch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
npm run build

npm start &lt;span class="c"&gt;# test production build locally&lt;/span&gt;

&lt;span class="c"&gt;# Then push to GitHub and deploy&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Lumen is a powerful tool for founders who want to move fast. It removes the friction of building a marketing site from scratch while remaining flexible enough to customize for any AI product.&lt;/p&gt;

&lt;p&gt;In a market where first impressions matter — and where founders have limited time — Lumen lets you ship a professional, conversion-focused landing page today. The template handles the design, performance, and SEO. You focus on the product and marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result?&lt;/strong&gt; A beautiful marketing site that doesn’t look like a template, loads like a dream, and converts like it was designed by a professional agency.&lt;/p&gt;

&lt;p&gt;Ship fast. Stand out. Win customers.&lt;/p&gt;

&lt;p&gt;— -&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Links
&lt;/h2&gt;

&lt;p&gt;buy : &lt;a href="https://boukataya.gumroad.com/l/tfwgq" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/tfwgq&lt;/a&gt;&lt;br&gt;
preview: &lt;a href="https://celadon-longma-d9d6cd.netlify.app/" rel="noopener noreferrer"&gt;https://celadon-longma-d9d6cd.netlify.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc06osux1ir1ncil7dka5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc06osux1ir1ncil7dka5.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fadefb8mcvmbxlrwh2m8y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fadefb8mcvmbxlrwh2m8y.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cr2o8sxw0ff4fhfx9jo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3cr2o8sxw0ff4fhfx9jo.png" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..." alt="Uploading image" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>design</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
