<?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: Ashif E.K</title>
    <description>The latest articles on DEV Community by Ashif E.K (@ashif-ek).</description>
    <link>https://dev.to/ashif-ek</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%2F4180025%2F57563a68-f77a-48ce-b10f-a43213f00ff5.jpg</url>
      <title>DEV Community: Ashif E.K</title>
      <link>https://dev.to/ashif-ek</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashif-ek"/>
    <language>en</language>
    <item>
      <title>Multi-Tenant SaaS: Creating a Schema Is Easy. Keeping Every Request in the Right One Is Hard.</title>
      <dc:creator>Ashif E.K</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:45:47 +0000</pubDate>
      <link>https://dev.to/ashif-ek/multi-tenant-saas-creating-a-schema-is-easy-keeping-every-request-in-the-right-one-is-hard-13dj</link>
      <guid>https://dev.to/ashif-ek/multi-tenant-saas-creating-a-schema-is-easy-keeping-every-request-in-the-right-one-is-hard-13dj</guid>
      <description>&lt;p&gt;When I started working on a multi-tenant SaaS architecture, one of the interesting challenges wasn't creating separate schemas in PostgreSQL.&lt;/p&gt;

&lt;p&gt;It was making sure every request consistently used the correct tenant's data.&lt;/p&gt;

&lt;p&gt;That sounds simple until you think about what happens when authentication, middleware, database sessions, and background jobs all need to agree on the same tenant context.&lt;/p&gt;

&lt;p&gt;I'm working with a multi-service architecture in WorkPilot AI, using FastAPI and PostgreSQL with schema-based tenant isolation. Here's an architectural lesson I've come to appreciate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating tenant schemas is only the beginning
&lt;/h2&gt;

&lt;p&gt;Imagine a SaaS application serving two organizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organization A uses &lt;code&gt;tenant_alpha&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Organization B uses &lt;code&gt;tenant_beta&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both organizations share the same PostgreSQL database, but their application data lives in separate schemas.&lt;/p&gt;

&lt;p&gt;The architecture might look 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;Incoming Request
      |
      v
Authentication
      |
      v
Resolve Tenant
      |
      v
Validate Access
      |
      v
Set Database Context
      |
      v
Execute Tenant-Scoped Query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important question isn't whether those schemas exist.&lt;/p&gt;

&lt;p&gt;It's whether the entire request lifecycle consistently respects the tenant boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things can go wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Trusting the wrong tenant identifier
&lt;/h3&gt;

&lt;p&gt;A request might contain a tenant ID, a subdomain, or a tenant-related claim in a JWT.&lt;/p&gt;

&lt;p&gt;But receiving a tenant identifier doesn't automatically make it trustworthy.&lt;/p&gt;

&lt;p&gt;The application must validate the tenant against a trusted source and verify that the authenticated user is authorized to access it.&lt;/p&gt;

&lt;p&gt;A client must never be able to access another organization's data simply by changing a request parameter.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Setting tenant context in middleware isn't enough
&lt;/h3&gt;

&lt;p&gt;Middleware can resolve the tenant and make the context available to downstream code.&lt;/p&gt;

&lt;p&gt;However, that context must also be applied correctly to the database session used for the request.&lt;/p&gt;

&lt;p&gt;If a query executes against the wrong schema, tenant resolution may appear to work while data isolation fails.&lt;/p&gt;

&lt;p&gt;This is why tenant resolution, authorization, and database session configuration should be treated as separate responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Database connection pooling introduces another consideration
&lt;/h3&gt;

&lt;p&gt;Applications commonly reuse database connections to avoid opening a new connection for every request.&lt;/p&gt;

&lt;p&gt;That's useful for performance, but it means database session state deserves careful attention.&lt;/p&gt;

&lt;p&gt;If tenant-specific session settings persist unexpectedly when a connection is reused, a later request could execute with the wrong context.&lt;/p&gt;

&lt;p&gt;With PostgreSQL, transaction-scoped settings and properly managed transaction boundaries can help prevent this class of problem. The exact approach depends on the database driver, connection pool, and transaction management strategy.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Background jobs don't automatically inherit request context
&lt;/h3&gt;

&lt;p&gt;Consider a task queued to Celery or another background worker.&lt;/p&gt;

&lt;p&gt;The original HTTP request might have resolved the tenant correctly, but the worker runs in a separate execution context.&lt;/p&gt;

&lt;p&gt;The job needs an explicit, validated tenant reference and must establish its own database context before accessing tenant-specific data.&lt;/p&gt;

&lt;p&gt;Never assume that request middleware has configured a background worker's database session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would focus on
&lt;/h2&gt;

&lt;p&gt;For a schema-based multi-tenant system, I would pay particular attention to these boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Establish who the user is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant resolution:&lt;/strong&gt; Determine which organization the request targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization:&lt;/strong&gt; Verify that the user can access that organization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database isolation:&lt;/strong&gt; Ensure queries execute within the intended tenant context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session lifecycle:&lt;/strong&gt; Prevent tenant state from leaking across requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background processing:&lt;/strong&gt; Re-establish tenant context in workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing:&lt;/strong&gt; Verify that one tenant cannot read or modify another tenant's records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd also test failure cases deliberately: missing tenant context, invalid tenant identifiers, unauthorized organization access, and reused database connections.&lt;/p&gt;

&lt;p&gt;A system that works for the happy path isn't necessarily a system that preserves tenant isolation under failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  One distinction worth remembering
&lt;/h2&gt;

&lt;p&gt;Schema-based multi-tenancy is an architectural strategy, not a complete security guarantee.&lt;/p&gt;

&lt;p&gt;Separate schemas can improve logical separation and organization, but the application still needs reliable authorization, safe database configuration, controlled migrations, and tests that verify isolation.&lt;/p&gt;

&lt;p&gt;The database structure alone doesn't enforce every security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  My takeaway
&lt;/h2&gt;

&lt;p&gt;The difficult part of multi-tenancy isn't creating a schema for every organization.&lt;/p&gt;

&lt;p&gt;It's preserving the correct tenant context across every layer that touches the data.&lt;/p&gt;

&lt;p&gt;Middleware, authentication, database sessions, connection pools, and background workers must work together. If even one layer assumes another layer has already handled tenant isolation, that assumption deserves to be tested.&lt;/p&gt;

&lt;p&gt;I'm still learning from building these systems, and this is one of the areas where thinking beyond the happy path makes a real difference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For developers building multi-tenant SaaS applications: how do you manage tenant context across database sessions and background jobs?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>postgres</category>
      <category>fullstack</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Logged-In User Shouldn't Have to Introduce Themselves to Your Chatbot</title>
      <dc:creator>Ashif E.K</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:20:26 +0000</pubDate>
      <link>https://dev.to/ashif-ek/a-logged-in-user-shouldnt-have-to-introduce-themselves-to-your-chatbot-26k3</link>
      <guid>https://dev.to/ashif-ek/a-logged-in-user-shouldnt-have-to-introduce-themselves-to-your-chatbot-26k3</guid>
      <description>&lt;p&gt;While working on an employee management platform, I ran into a problem that looked small at first.&lt;/p&gt;

&lt;p&gt;The user was already logged in.&lt;/p&gt;

&lt;p&gt;The application knew who they were. Their role was available. Their session was active.&lt;/p&gt;

&lt;p&gt;Yet, when they opened the AI assistant and asked about their attendance or leave, the assistant could still ask them to provide their employee ID or name.&lt;/p&gt;

&lt;p&gt;Something wasn't connected properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem wasn't the chatbot's intelligence
&lt;/h2&gt;

&lt;p&gt;My first instinct was to think about how to improve the assistant's instructions.&lt;/p&gt;

&lt;p&gt;But the more useful question was: why does the assistant need to ask for information that the application already has?&lt;/p&gt;

&lt;p&gt;A chatbot prompt can tell an AI how to behave. It cannot magically give the backend the correct identity, permissions, or database context.&lt;/p&gt;

&lt;p&gt;If those things aren't connected properly, even a well-written prompt can produce a frustrating experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I started separating
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Identity: Who is making this request?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This should come from the authenticated session or a verified access token.&lt;/p&gt;

&lt;p&gt;The user shouldn't need to introduce themselves every time they open a feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Authorization: What are they allowed to do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Knowing someone's identity doesn't mean they can access every resource.&lt;/p&gt;

&lt;p&gt;An employee might be allowed to view their own attendance, while an HR administrator may have permission to manage attendance records for multiple employees.&lt;/p&gt;

&lt;p&gt;The backend must enforce these permissions. The AI should not decide them on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Data scope: Which records should this request use?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even after identifying the user and checking their permissions, the application must scope the database query correctly.&lt;/p&gt;

&lt;p&gt;For example, an employee asking about their leave balance should get data associated with their authenticated employee record—not whichever employee ID happens to appear in a chat message.&lt;/p&gt;

&lt;p&gt;These are separate responsibilities. Treating them as one problem creates confusing behavior and can introduce security vulnerabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the backend should do instead
&lt;/h2&gt;

&lt;p&gt;A simplified version of the flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_my_leave_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;employee&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;get_employee_for_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&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;if&lt;/span&gt; &lt;span class="n"&gt;employee&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;EmployeeNotFoundError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_leave_balance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;employee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&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;This is illustrative pseudocode; the helper functions and exception would need to be implemented in the actual application.&lt;/p&gt;

&lt;p&gt;The important part is where &lt;code&gt;employee.id&lt;/code&gt; comes from: a trusted relationship between the authenticated user and their employee record.&lt;/p&gt;

&lt;p&gt;The client or chatbot shouldn't be able to choose another employee's identity just by supplying a different ID.&lt;/p&gt;

&lt;p&gt;For administrative actions, the backend would also need to verify the user's role and the specific permission required for that operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bigger lesson
&lt;/h2&gt;

&lt;p&gt;Building AI features has made one thing particularly clear to me: adding an AI model doesn't remove the need for good backend architecture.&lt;/p&gt;

&lt;p&gt;Authentication, authorization, data access, and business rules still need to work correctly.&lt;/p&gt;

&lt;p&gt;The AI can help interpret a request. The application must determine whose request it is, what that person can access, and which data the answer should be based on.&lt;/p&gt;

&lt;p&gt;That distinction matters even more when the assistant can do things rather than simply answer questions.&lt;/p&gt;

&lt;p&gt;I'm still learning, building, and finding these gaps in real projects. And sometimes, the most useful engineering lesson comes from a feature that technically works but still behaves in a way that makes no sense to the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My takeaway:&lt;/strong&gt; Don't ask the user to repeat information your system should already know. Pass the right context through the right layers, and enforce the important rules on the backend.&lt;/p&gt;

&lt;p&gt;I'd be interested to hear how other developers handle authenticated user context in AI assistants, especially in applications with multiple roles and organizations.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>python</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
