<?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: WISSEN BERATUNG</title>
    <description>The latest articles on DEV Community by WISSEN BERATUNG (@wb_crm).</description>
    <link>https://dev.to/wb_crm</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3541139%2Fa5a3da44-2f1b-4c5f-b41f-cbac4b678ff3.png</url>
      <title>DEV Community: WISSEN BERATUNG</title>
      <link>https://dev.to/wb_crm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wb_crm"/>
    <language>en</language>
    <item>
      <title>Multi-Tenancy in Laravel: Database-per-Tenant vs. Shared Database — A Practical Guide</title>
      <dc:creator>WISSEN BERATUNG</dc:creator>
      <pubDate>Fri, 03 Apr 2026 20:02:30 +0000</pubDate>
      <link>https://dev.to/wb_crm/multi-tenancy-in-laravel-database-per-tenant-vs-shared-database-a-practical-guide-3f0j</link>
      <guid>https://dev.to/wb_crm/multi-tenancy-in-laravel-database-per-tenant-vs-shared-database-a-practical-guide-3f0j</guid>
      <description>&lt;p&gt;If you're building a multi-tenant SaaS with Laravel, you'll face this decision early: &lt;strong&gt;shared database&lt;/strong&gt; (all tenants in one DB with a &lt;code&gt;tenant_id&lt;/code&gt; column) or &lt;strong&gt;database-per-tenant&lt;/strong&gt; (each tenant gets their own database).&lt;/p&gt;

&lt;p&gt;After running a database-per-tenant architecture in production for WB-CRM, here's a practical comparison.&lt;/p&gt;

&lt;h4&gt;
  
  
  Shared Database (Single-DB)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Every query needs tenant scoping&lt;/span&gt;
&lt;span class="nc"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tenant_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$currentTenantId&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;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Risk: forget the scope → data leak&lt;/span&gt;
&lt;span class="nc"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Returns ALL tenants' data!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Simple migrations, easy cross-tenant queries, lower operational complexity.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; One bug = cross-tenant data exposure. GDPR Art. 17 deletion is complex (cascade through all tables). Hard to offer dedicated instances.&lt;/p&gt;
&lt;h4&gt;
  
  
  Database-per-Tenant
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stancl/tenancy switches the default DB connection&lt;/span&gt;
&lt;span class="c1"&gt;// No tenant_id needed — the database IS the scope&lt;/span&gt;
&lt;span class="nc"&gt;Lead&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Returns only current tenant's data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Architecturally impossible to leak data. Trivial backup/restore/deletion per tenant. Can offer dedicated instances.&lt;br&gt;
&lt;strong&gt;Cons:&lt;/strong&gt; Migrations run N times. Cross-tenant queries require connection switching. More complex connection pooling.&lt;/p&gt;
&lt;h4&gt;
  
  
  The Practical Reality
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Migrations:&lt;/strong&gt; We have ~80 tenant migrations. With 50 tenants, that's 4,000 migration operations per deploy. We parallelize via queue workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan tenants:migrate &lt;span class="nt"&gt;--parallel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Central vs. Tenant models:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Central model — MUST set connection explicitly&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'central'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Tenant model — NO $connection property (rely on bootstrapper)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Lead&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// stancl/tenancy handles connection switching&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The #1 gotcha:&lt;/strong&gt; If you put &lt;code&gt;$connection = 'mysql'&lt;/code&gt; on a central model instead of &lt;code&gt;$connection = 'central'&lt;/code&gt;, it will silently query the tenant database when tenancy is initialized.&lt;/p&gt;

&lt;h4&gt;
  
  
  When to Choose What
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Shared DB&lt;/th&gt;
&lt;th&gt;DB-per-Tenant&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 100 tenants&lt;/td&gt;
&lt;td&gt;Either works&lt;/td&gt;
&lt;td&gt;Recommended for B2B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GDPR compliance critical&lt;/td&gt;
&lt;td&gt;Possible but harder&lt;/td&gt;
&lt;td&gt;Recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-tenant analytics&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;Requires extra work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated instances&lt;/td&gt;
&lt;td&gt;Not possible&lt;/td&gt;
&lt;td&gt;Natural extension&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Lower&lt;/td&gt;
&lt;td&gt;Slightly higher&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For B2B SaaS in regulated industries (healthcare, finance, legal), database-per-tenant is worth the operational overhead. For B2C with millions of users, shared DB makes more sense.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with these patterns: &lt;a href="https://wb-crm.net" rel="noopener noreferrer"&gt;WB-CRM&lt;/a&gt; — free CRM for SMBs, hosted in Germany.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>multitenancy</category>
      <category>saas</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Building a GDPR-Compliant Multi-Tenant CRM with Laravel</title>
      <dc:creator>WISSEN BERATUNG</dc:creator>
      <pubDate>Fri, 03 Apr 2026 19:59:44 +0000</pubDate>
      <link>https://dev.to/wb_crm/building-a-gdpr-compliant-multi-tenant-crm-with-laravel-3n1o</link>
      <guid>https://dev.to/wb_crm/building-a-gdpr-compliant-multi-tenant-crm-with-laravel-3n1o</guid>
      <description>&lt;p&gt;Building a CRM that handles personal data (names, emails, phone numbers, addresses) in the EU means you can't treat GDPR as an afterthought. Here's how we implemented it in WB-CRM, our multi-tenant CRM built with Laravel 12.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Database-per-Tenant Architecture
&lt;/h4&gt;

&lt;p&gt;We use &lt;a href="https://tenancyforlaravel.com/" rel="noopener noreferrer"&gt;stancl/tenancy&lt;/a&gt; v3 with database-per-tenant isolation. Each tenant gets their own MySQL database (&lt;code&gt;tenant_acme&lt;/code&gt;, &lt;code&gt;tenant_demo&lt;/code&gt;, etc.).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Central models explicitly set their connection&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'central'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Tenant models rely on the bootstrapper — no $connection property&lt;/span&gt;
&lt;span class="c1"&gt;// stancl/tenancy switches the default connection automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why not shared database with row-level security?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a shared database, one missing &lt;code&gt;WHERE tenant_id = ?&lt;/code&gt; clause leaks data across companies. With DB-per-tenant, it's architecturally impossible.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Field-Level Encryption for PII
&lt;/h4&gt;

&lt;p&gt;Laravel's &lt;code&gt;encrypted&lt;/code&gt; cast encrypts individual database fields with AES-256:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;casts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'name'&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'encrypted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'email'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'encrypted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'phone'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'encrypted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'address'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'encrypted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ip_address'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'encrypted'&lt;/span&gt;&lt;span class="p"&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;&lt;strong&gt;The tradeoff:&lt;/strong&gt; You can't query encrypted fields with &lt;code&gt;WHERE email = ?&lt;/code&gt;. We solve this with companion hash columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// email_hash stores a HMAC-SHA256 hash for lookups&lt;/span&gt;
&lt;span class="nv"&gt;$contact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Contact&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'email_hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;hash_hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.key'&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;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. GDPR Data Subject Rights in Code
&lt;/h4&gt;

&lt;p&gt;Articles 15-21 are not just checkboxes — they need actual endpoints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Article&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;Art. 15&lt;/td&gt;
&lt;td&gt;JSON/CSV export endpoint with re-authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rectification&lt;/td&gt;
&lt;td&gt;Art. 16&lt;/td&gt;
&lt;td&gt;Profile edit functionality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Erasure&lt;/td&gt;
&lt;td&gt;Art. 17&lt;/td&gt;
&lt;td&gt;Account deletion + cascading DB cleanup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restriction&lt;/td&gt;
&lt;td&gt;Art. 18&lt;/td&gt;
&lt;td&gt;Account freeze (disable without delete)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability&lt;/td&gt;
&lt;td&gt;Art. 20&lt;/td&gt;
&lt;td&gt;Machine-readable export (JSON)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Objection&lt;/td&gt;
&lt;td&gt;Art. 21&lt;/td&gt;
&lt;td&gt;Marketing opt-out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every GDPR operation writes an audit log entry with: who, when, what, which tenant, old values, new values.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Audit Trail
&lt;/h4&gt;

&lt;p&gt;Every state-changing operation produces an audit log entry. This is mandatory for GDPR Art. 30 (records of processing activities):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;TenantAuditLog&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'uuid'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'auditable_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;get_class&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'auditable_id'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$model&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'event'&lt;/span&gt;          &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'gdpr_data_export'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'old_values'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'new_values'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'format'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'fields'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$exportedFields&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;'user_type'&lt;/span&gt;      &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;TenantUser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'user_id'&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$tenantUser&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="s1"&gt;'ip_address'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;ip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s1"&gt;'user_agent'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;request&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;userAgent&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;h4&gt;
  
  
  5. What I Wish I Knew Earlier
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Herd/CLI bcrypt incompatibility&lt;/strong&gt;: CLI PHP and Herd PHP on macOS produce incompatible bcrypt hashes. Always set passwords from the web context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session connection must be &lt;code&gt;central&lt;/code&gt;&lt;/strong&gt;: With database-per-tenant, sessions stored in a tenant DB are lost when switching tenants.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ENUM columns are evil in migrations&lt;/strong&gt;: Adding a value requires recreating the column in MySQL. Use string columns with validation instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;getCustomColumns()&lt;/code&gt; in stancl/tenancy&lt;/strong&gt;: If you add a column to the tenants table but forget to register it in &lt;code&gt;getCustomColumns()&lt;/code&gt;, it silently lands in the JSON &lt;code&gt;data&lt;/code&gt; column. Hours of debugging.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;We built &lt;a href="https://wb-crm.net" rel="noopener noreferrer"&gt;WB-CRM&lt;/a&gt; with these principles. Free ONE plan available (500 contacts, 1 user). Built and hosted in Germany.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>saas</category>
      <category>php</category>
      <category>laravel</category>
    </item>
  </channel>
</rss>
