<?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: Gaurav Digari</title>
    <description>The latest articles on DEV Community by Gaurav Digari (@gaurav_digari).</description>
    <link>https://dev.to/gaurav_digari</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%2F3996154%2F8a380120-3aba-4197-90da-03b865144e5c.jpeg</url>
      <title>DEV Community: Gaurav Digari</title>
      <link>https://dev.to/gaurav_digari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaurav_digari"/>
    <language>en</language>
    <item>
      <title>I got tired of rebuilding Flask auth from scratch — so I built a proper starter kit</title>
      <dc:creator>Gaurav Digari</dc:creator>
      <pubDate>Mon, 22 Jun 2026 05:34:08 +0000</pubDate>
      <link>https://dev.to/gaurav_digari/i-got-tired-of-rebuilding-flask-auth-from-scratch-so-i-built-a-proper-starter-kit-2d7a</link>
      <guid>https://dev.to/gaurav_digari/i-got-tired-of-rebuilding-flask-auth-from-scratch-so-i-built-a-proper-starter-kit-2d7a</guid>
      <description>&lt;p&gt;Every Flask project I started followed the same painful pattern.&lt;/p&gt;

&lt;p&gt;Day 1: excited about the idea.&lt;br&gt;
Day 2: still setting up signup and login.&lt;br&gt;
Day 3: debugging OTP email delivery.&lt;br&gt;
Day 4: building an admin panel to manage users.&lt;br&gt;
Day 5: finally writing the first line of the actual idea.&lt;/p&gt;

&lt;p&gt;Sound familiar?&lt;/p&gt;

&lt;p&gt;After the third time doing this, I stopped and built it properly once — a production-ready Flask authentication and admin foundation I could drop into any new project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually built
&lt;/h2&gt;

&lt;p&gt;Not a tutorial-level "here's how Flask-Login works" setup. A real, production-ready codebase with actual security practices:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup and login with secure session handling&lt;/li&gt;
&lt;li&gt;Email OTP verification — 6-digit code, expires in 10 minutes, resend with cooldown&lt;/li&gt;
&lt;li&gt;Password complexity enforced both client-side and server-side (not just a frontend hint)&lt;/li&gt;
&lt;li&gt;Forgot/reset password with single-use, expiring tokens&lt;/li&gt;
&lt;li&gt;Passwords hashed with bcrypt — never stored plain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting on login, OTP requests, and password reset (Flask-Limiter)&lt;/li&gt;
&lt;li&gt;CSRF protection on every form&lt;/li&gt;
&lt;li&gt;Security headers (X-Frame-Options, X-Content-Type-Options, Referrer-Policy)&lt;/li&gt;
&lt;li&gt;Cache-control headers that prevent browsers from showing stale authenticated pages on back/forward navigation&lt;/li&gt;
&lt;li&gt;Input validation and sanitization on every endpoint&lt;/li&gt;
&lt;li&gt;No secrets hardcoded anywhere — everything from environment variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Admin panel&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboard with total users, active users, new signups this week&lt;/li&gt;
&lt;li&gt;Searchable, paginated user table&lt;/li&gt;
&lt;li&gt;Enable/disable, promote to admin, or delete users&lt;/li&gt;
&lt;li&gt;Admins land directly on the admin panel after login — no unnecessary redirect through the user dashboard&lt;/li&gt;
&lt;li&gt;Cascade-safe user deletion (no orphaned OTP records crashing the delete)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Code quality&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flask application factory pattern with Blueprints (auth, admin, main)&lt;/li&gt;
&lt;li&gt;SQLAlchemy ORM with Flask-Migrate for database migrations&lt;/li&gt;
&lt;li&gt;Environment-based config (development/production/testing)&lt;/li&gt;
&lt;li&gt;pytest test suite covering signup, OTP verification, login, wrong password, unverified account blocking&lt;/li&gt;
&lt;li&gt;Background email sending — SMTP happens on a thread so the page responds instantly instead of waiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;UI&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tailwind CSS via CDN — zero build step required&lt;/li&gt;
&lt;li&gt;Custom fonts (Outfit + Inter + JetBrains Mono)&lt;/li&gt;
&lt;li&gt;Responsive split-panel auth layout&lt;/li&gt;
&lt;li&gt;Show/hide password toggle on every password field&lt;/li&gt;
&lt;li&gt;Flash messages with close button and 5-second auto-dismiss&lt;/li&gt;
&lt;li&gt;Custom error pages for 403, 404, 429, 500 — no raw stack traces ever shown&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I learned building this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Background threads for email sending matter more than you think.&lt;/strong&gt; My first version blocked the request while the SMTP connection happened. A misconfigured hostname would hang the entire request for 30+ seconds with no feedback to the user. Moving email dispatch to a daemon thread made every auth action feel instant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The browser cache is a security issue.&lt;/strong&gt; Without explicit Cache-Control: no-store headers, pressing back after logout can show a stale authenticated page directly from browser memory — the server never even gets asked. Most Flask tutorials don't cover this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cascade deletes need explicit configuration.&lt;/strong&gt; SQLAlchemy's default behavior when you delete a User with related OTPCode rows is to try setting user_id = NULL on those rows — which fails immediately if user_id is NOT NULL (as it should be). You need cascade="all, delete-orphan" on the relationship, not just the foreign key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSP blocks your own inline scripts.&lt;/strong&gt; I added a Content-Security-Policy header that blocked the inline Tailwind config script defining my custom color tokens. Every button existed and worked but rendered invisible — white text on white background. Spent longer than I'd like to admit on that one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.10+, Flask 3.0&lt;/li&gt;
&lt;li&gt;SQLAlchemy + Flask-Migrate&lt;/li&gt;
&lt;li&gt;Flask-Login, Flask-WTF, Flask-Mail, Flask-Limiter&lt;/li&gt;
&lt;li&gt;Tailwind CSS (CDN)&lt;/li&gt;
&lt;li&gt;SQLite for development, Postgres-ready for production&lt;/li&gt;
&lt;li&gt;pytest&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to get it
&lt;/h2&gt;

&lt;p&gt;If you're building a Flask project and don't want to spend days on auth infrastructure, it's available here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://gauav.gumroad.com/l/flask-saas-starter-kit" rel="noopener noreferrer"&gt;Flask SaaS Starter Kit on Gumroad&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Comes with a README, a step-by-step SETUP guide written for people who aren't Flask experts yet, and a CUSTOMIZE guide for extending the user model and swapping email providers.&lt;/p&gt;

&lt;p&gt;Happy to go deep on any of the implementation decisions — the rate limiting strategy, the OTP flow design, session security, the background email threading. Drop a comment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does your auth setup usually look like for new Flask projects?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>flask</category>
      <category>python</category>
      <category>webdev</category>
      <category>security</category>
    </item>
  </channel>
</rss>
