
10 Things to Get Right When You're Vibe Coding Your First SaaS
A SaaS foundations checklist for vibe coders
Here's the thing: I almost didn't write this.
Because honestly? In a year or two, you probably won't care how things are written. AI will be good enough that you hand it a problem and it just... solves it. Architecturally sound. Well-structured. Production-ready.
We're probably not there yet. But we're not far off.
Right now, the difference between AI producing scattered, unmaintainable code and clean, well-architected code comes down to one thing: the specificity of your prompts. If you're aware of terms like middleware, RLS, or GUID, you can ask for them directly. The more precise your vocabulary, the smarter the output.
These are navigation points—hints to the LLM. You don't need to deeply understand what these terms mean. This article will help you learn the right terminology to use in your prompts so AI can do the heavy lifting.
I've spent twenty years building SaaS products and working with dozens of early and mid-stage teams. I've watched the same architectural mistakes happen over and over, and they're brutal.
One team skipped tenanting middleware entirely. Queries got duplicated everywhere. Reports started showing other customers' data. They'd miss data. Fix one thing, break another. By the time they realised the problem, they were rewriting half their codebase.
Another startup burnt through a million dollars deploying dozens of microservices because it felt like the "right" way to build. Then they expected one or two developers to manage all of it. They'd picked out-of-date languages because "we thought it would be cool." The operational overhead crushed them.
A third had no architectural layers at all. Their server actions were littered with the same authentication checks, permission lookups, and data validation code repeated over and over. Every time they added a feature, they duplicated the mess. Changing how auth worked meant touching fifty files.
These aren't failures of competence. Sometimes they're not even failures of intentionality—the teams just didn't know. They were in the same position as any vibe coder starting out today. That's exactly why I'm writing this.
Here are the 10 things I think are among the most important when building your first SaaS. I've included the key terminology and a prompt for each one—the exact words to use when talking to Claude or Cursor to get smarter code back. Get these right from the start and you'll skip years of refactoring and regret.
1. One Git Repo, Deploy Everything at Once
Microservices and API-first architecture are antipatterns for you right now. You're solo. You have one product. You need one Git repository with your backend and frontend living together, deploying in one go.
Here's the reality: maintaining two repositories, two deployment pipelines, two separate versions, and keeping them in sync is overhead you don't need. You have exactly one customer: your users. You don't have team silos. You don't have scaling constraints that require separate services.
Keep it simple. One repo. Everything deploys together.
Prompt:
I'm building a SaaS product as a solo developer. I want everything in a single Git repository—backend, frontend, database—deploying together.
Recommend a technology stack that:
1. Works well in a mono-repo setup (shared types, single deployment pipeline)
2. Has strong AI tooling support (copilots, code generation, documentation)
3. Is simple enough for one developer to maintain
4. Has a clear path to production (hosting, CI/CD, database)
5. Minimises operational overhead
For each recommendation, explain why it fits mono-repo development specifically.Tip: One Git repo means one version of truth. You change a database schema, you update the frontend code that uses it in the same commit, in the same pull request. No coordination between services. No "we deployed the backend but the frontend isn't updated yet" bugs. No version mismatches. No API documentation drift. Your CI/CD pipeline is simple: code → test → build → deploy. That's it.
2. Small Files, One Thing Each
Keep your components and functions small. One responsibility per file. This isn't about being pedantic—it's about making it easier for AI to help you and easier to maintain your code.
Small, focused files fit into AI context windows more easily. When you hand Claude a focused file instead of a 1000-line monster, the whole thing fits in context and the output quality changes completely.
Prompt:
I'm trying to keep my components small and focused, but I'm not sure where to draw the line.
Give me principles for:
1. When to split a component into smaller pieces
2. How small is "small" (lines of code, number of responsibilities, etc.)
3. How to handle related components (should they be in the same file or separate?)
4. File naming conventions that make it clear what each component does
5. Single responsibility principle applied to code
I'm using [React/your framework]. Show examples of good component structure.Tip: If you're scrolling more than once to see a whole component or function, it's too big. If you're thinking "well this also handles X and Y" while writing it, split it. Small files aren't about being precious—they're about making it so AI can hold the whole thing in context and make smart changes without accidentally breaking something else.
3. Tenanting: Keep It Simple, But Intentional
Tenanting is the boundary where customer data cannot flow between. One account, one customer, one set of data—completely isolated from every other customer. And crucially: store it all in one database. You don't need separate databases for each tenant. That's complexity you don't need. One database, with access controls to keep data separated. That's it.
How you implement this depends on your stack. At the database level, PostgreSQL has RLS (Row-Level Security)—policies that filter rows automatically based on the current user context. At the application level, you've got default scopes in Rails (or gems like acts_as_tenant) or query filters in .NET Entity Framework that automatically add tenant conditions to every query. Pick one approach and stick with it. Database-level RLS is more secure (queries can't accidentally bypass it), but app-level scopes are easier to debug and more portable across databases.
Decide upfront what's tenant data (belongs to one organisation) and what's global data (shared across all organisations, like your user profiles or payment info). When you're writing queries, think about whose data you're touching. Is it globally accessible? Should it be?
One thing people miss: your admin interface needs to bypass tenanting restrictions to access cross-account data. You'll need to be able to log in as a user from any organisation, view any organisation's data, and understand the full picture. This means your permission checks can't just be "is this data in my org?" They need an escape hatch: "am I an admin?" Build that escape hatch into your permission logic from day one, because retrofitting it later means touching every query in your codebase.
Prompt:
I'm building a SaaS with account-based tenanting using a single shared database.
Each customer has their own account and their data should be completely isolated
via row-level access controls, not separate databases. I'm using [Supabase/PostgreSQL/your DB].
Help me set up the basic RLS policies so that:
1. Users can only see their own organisation's data
2. Admins within an org can manage org-level permissions
3. Developers/admin accounts can bypass RLS to access any organisation's data for debugging
4. Public queries are explicitly marked as such
Show me the schema and RLS policies needed to start with.Tip: Make a deliberate list of what's tenant data vs global data before you start building. Tenant data: projects, posts, comments, workflows, anything created within an organisation. Global data: user accounts, authentication credentials, billing info, org metadata. When you're unsure, default to tenant data—it's easier to make something global later than to isolate it. And here's the critical bit: when implementing your admin bypass, don't scatter if user.is_admin? return all data checks everywhere. Put that logic in one middleware layer so when you need to change how admin access works, you change it once.
4. Middleware Is Your Foundation, Not Your Afterthought
A cross-cutting concern is something that happens on almost every request but doesn't belong in any specific business logic. These patterns apply everywhere: logging, authentication, authorisation, auditing. They're not features of one endpoint—they're patterns that need to happen everywhere.
Think about what needs to happen on every request. Who are you? What should you have access to? Should we log this?
Get these patterns in place early. Keep it simple though. You need basic request logging and authentication middleware from day one. Soft deletion is useful but not essential yet—know about it, but don't stress if you skip it initially. You can add it later if you need the ability to recover deleted data.
Prompt:
I'm building [Rails/Express/FastAPI] API and want to set up middleware patterns from day one.
I need:
1. Request logging that captures user_id, org_id, action, timestamp
2. Authentication middleware that validates JWT and attaches user context
3. Basic audit fields on every record (created_by, created_at, updated_by, updated_at)
Show me the implementation for each middleware component and how they work together.Tip: Open up two different endpoints or server actions (commonly called controllers in Rails or .NET) side by side. Look at your create endpoint and your update endpoint. Are you checking permissions in both? Looking up the organisation in both? Validating the user has access in both? If you see the same permission checks, parameter validations, or user lookups repeated across multiple endpoints, you're missing middleware. That repeated code is your red flag. Extract those checks into a single middleware layer that runs before your endpoint logic. Your endpoints should only care about business logic, not permission checks or authentication. If you're writing the same if (!user || user.org_id !== org_id) check in three different places, move it to middleware and reference it once.
One more thing: soft deletion is worth knowing about. Instead of actually deleting a record, you mark it as deleted with a deleted_at timestamp. This saves you when a customer accidentally deletes something important or you need to understand what happened. But it's not essential on day one—add it when you feel the pain of needing it.
5. Decide Your Tenanting Strategy: Subdomains vs. Path-Based
Are your customers accessing their data at customer.yoursaas.com or /orgs/customer?
This isn't just about how tenants access their data—it's the start of a bigger conversation about your URL being state. The URL tells you who the customer is, what they're looking at, and how to fetch the right data. We'll cover URL-as-state more in Section 10, but this is where it begins.
This decision affects authentication, cookies, SSL certificates, your whole mental model. Can you retrofit it later? Sure. But why bother? Get it right from day one and move forward consistently.
Prompt:
I'm deciding between two tenanting strategies for my SaaS:
Option A: Subdomain-based (acme.myapp.com, widgets.myapp.com)
Option B: Path-based (/orgs/acme, /orgs/widgets)
For [your framework], what are the pros/cons of each for:
1. Authentication & session handling
2. SSL/HTTPS setup
3. Database queries (how do I know which tenant the request is for?)
4. Local development
5. Scaling
Which would you recommend and why?Tip: Path-based tenanting is simpler for most SaaS. No wildcard DNS setup, no certificate complexity, easier local development. Subdomain-based feels more "enterprise" but adds complexity. Pick path-based unless you have a specific reason not to (like branded subdomains for white-label setups). Once you pick one, stick with it. Switching halfway through is a nightmare.
6. Users Are Global, Organisations Are Not
A user can exist globally. An organisation is a boundary. Users belong to organisations. Users have roles within those organisations.
Build this relationship now, not when you're trying to bolt permissions onto an existing system.
Prompt:
I need to set up user/organisation/role relationships for a multi-tenant SaaS.
Schema requirements:
- Users table (global across all orgs)
- Organisations table
- A way to assign users to orgs with specific roles (admin, editor, viewer, etc.)
- A way to check if a user has a specific role in an org
Show me the database schema, the associations in [Rails/your ORM], and a few utility methods
like current_user.role_in(org) or user.can_edit?(org).Tip: For your initial release, hardcoded permissions per role is totally fine. "admin can do everything, editor can edit, viewer can view" works. Just know that eventually you might want a permission matrix where roles are collections of granular permissions. This gives you flexibility when a customer asks for a custom role that has some admin permissions but not others. But that's a future step—don't over-engineer it on day one.
If you're using Rails, Rolify (role management) and Pundit (authorisation policies) are battle-tested gems that work beautifully together. Even if you're not using Rails, these are great keywords to drop into your prompts: "I need something like Rolify and Pundit for [your framework]"—the LLM will know exactly what patterns you're looking for.
7. GUIDs for IDs (Seriously, Don't Leak Information)
Sequential IDs tell the world how many customers you have and in what order you signed them up.
Compare these two URLs:
-
/orgs/42/projects/1337— You're customer #42. You have 1,337 projects. Your competitors can see that. -
/orgs/a1b2c3d4/projects/e5f6g7h8— Good luck figuring out anything from that.
Use GUIDs (also called UUIDs). Yes, they're longer. Yes, they look less clean in logs. They don't leak information across tenants and that's worth it from day one.
Some stacks give you this for free—Supabase and many Next.js templates default to UUIDs out of the box. But Rails and .NET default to sequential integers, so you'll need to explicitly configure UUID primary keys. Check your framework's defaults and fix it early—retrofitting UUIDs onto an existing database is painful.
Prompt:
I want to use UUIDs instead of sequential IDs for all my models to avoid information leakage.
For [Rails/your framework]:
1. Show me how to set UUID as the default primary key
2. How to generate UUIDs on model creation
3. Best practices for UUID performance in queries
Set this up so it's the default for every new model I create.Tip: GUIDs aren't just about security—they make debugging easier. When a customer sends you an error message with their project ID, you can't guess how many other projects exist. You can't accidentally figure out which customers are big or small based on ID sequences. It's a small thing that makes your system feel more professional and secure.
8. Slugs and Friendly URLs
Your database uses GUIDs. Your URLs don't have to.
Users should see /projects/my-awesome-project not /projects/550e8400-e29b-41d4-a716-446655440000. Slugs are human-readable, shareable, and way better for SEO.
Use a slug field on your model. Make it unique within the organisation. Route based on slug in your app, but query by GUID in the database. Best of both worlds.
Rails has built-in support for this with gems like friendly_id. Most other languages have similar libraries—don't reinvent the wheel here.
Prompt:
I'm using UUIDs as primary keys but want user-friendly URLs with slugs.
For [Rails/your framework]:
1. Add a slug column to my [Project/Post/Resource] model
2. Auto-generate slugs from the title (my-awesome-project from "My Awesome Project")
3. Handle slug uniqueness within an organisation (same slug can exist in different orgs)
4. Route setup: URLs should look like /orgs/my-org/projects/my-project-slug
5. When querying, use the slug to find the record but use GUID internally
6. Handle slug changes (when someone updates the title, do we update the slug? Redirect old slug?)
Show me the model logic, migrations, routes, and controller actions.Tip: Slugs should be immutable or have redirect support. If you change a slug every time the title changes, users' bookmarks break and you lose SEO juice. Either keep the slug stable (don't regenerate on title change) or add a redirect from the old slug to the new one. Your future self will thank you when someone complains about broken links from months ago.
9. Admin Access Is Not Optional
You're going to have customers with problems. You need to see what they're seeing to understand what's happening. If you don't build impersonation from the beginning, you'll spend hours trying to reproduce issues.
Now, if you've got multiple organisations per user set up, you can sometimes just add yourself as an admin to their organisation and poke around. That works. But true impersonation—logging in as them—is different. You see exactly what they see, with their permissions, their data, their view. Not expanded access. Not the admin view. Their actual experience. That's invaluable for debugging.
It's a few lines of code. Add it when you can.
Prompt:
I need to add impersonation to my SaaS so I can log in as other users to debug issues.
Requirements:
1. Only developers/admins can impersonate
2. Log all impersonation attempts
3. Show a clear indicator in the UI that someone is impersonated
4. Easy way to exit impersonation and return to admin account
5. Can't impersonate other admins (safety)
Show me the controller logic, session handling, and UI components needed.Tip: Make impersonation obvious. Add a banner at the top of the screen that says "You are impersonating Jane Doe" in a bright colour. It should be impossible to forget you're in admin mode. A visual indicator saves debugging time and prevents accidental customer data changes.
10. URL Is State—Use It, Don't Fight It
Your URL is not just a pretty thing. It's how users bookmark. It's how they share. It's how they navigate.
When someone filters your data, is that reflected in the URL as query parameters? When they click back, do they go back to where they were? Build with this in mind.
One of the most common mistakes: tabs. Interfaces often use tabs to show different views of the same data—"Overview", "Activity", "Settings". If you're not updating the URL when users switch tabs, they lose that context on refresh. They share a link and their colleague lands on the wrong tab. Small thing, big frustration.
Prompt:
I want to make sure my URLs encode state properly so users can bookmark and share filtered views.
Example: A user filters projects by status="active" and sort="updated_at".
I want the URL to be ?status=active&sort=updated_at and when they click back,
they should see the same filtered view.
Show me how to:
1. Extract query params and use them to filter data
2. Update the URL when filters change (without page reload)
3. Initialize the page with filters from the URL on load
4. Handle edge cases (invalid params, etc.)
I'm using [React/your frontend framework].Tip: The browser's back button is free UX. Use it. Every time a user clicks a filter, update the query string (everything after the ?). Every time they navigate away and come back, restore from the URL. You don't need to store filter preferences in a database if they're in the query parameters. The URL is the state.
Here's how to test if you're doing it right: Apply some filters. The URL should change—you should see ?status=active&sort=date. Click away to another page. Hit back. Did your filters disappear? You're doing it wrong. Copy your current URL and paste it to a colleague. Do they see exactly what you're seeing? No? You're doing it wrong.
Remember: #anchors don't get sent to the server, so use ?query=params for state that matters. The server sees everything before the #—the path and the query string. It doesn't see the hash. So if you're storing important state in the hash, it won't survive a page refresh. Put it in the query string. That's the contract between client and server—everything before the # is state that persists everywhere.
Wrapping Up
The terminology we've covered here? It takes two minutes to drop into your prompts. And you'll see an instant improvement in the quality of what the LLM gives you back.
Understanding these terms and using them in your prompts is how you guide AI to build something that actually scales and makes sense. It saves you months of refactoring later.
The future of building doesn't mean you skip foundations. It means you get them done faster with AI help, then actually focus on building something people want to use.
Here's the terminology that matters. Use these words in your prompts:
Term | What It Means |
|---|---|
| Breaking your app into separate deployable services—overkill for solo builders |
| Building your backend as a standalone API separate from frontend—one of the most broken design patterns we ever adopted as an industry, in my opinion |
| The dividing line where data should NOT leak. One customer's data stays isolated from another's |
| The entity that represents a customer account—the boundary for tenant data |
| Database-level (PostgreSQL RLS) or app-level (Rails default_scope, .NET query filters) access control that keeps tenant data separate |
| Something that happens on every request (logging, auth, auditing) |
| Layer that runs before your business logic, handles common patterns |
| Where your endpoint logic lives—the code that handles requests |
| Recording what happened (requests, errors, actions) for debugging and auditing |
| Verifying who a user is (login, tokens, sessions) |
| Verifying what a user can do (permissions, roles, access control) |
| Tracking what happened and who did it for accountability and debugging |
| The message sent from the browser to the server asking it to do something |
| Unique ID that doesn't leak information about your customers |
| Human-readable URL path based on content (my-project vs GUID) |
| Sending users from an old URL to a new one so bookmarks don't break |
| State in the URL after the ? that the server sees |
| Logging in as another user to see exactly what they see |
| Marking a record as deleted instead of removing it |
Understand the terminology. Use it in your prompts. Watch the quality of what you build improve.
Build smart. Ship fast. 🚀

