pg_multitenant_schemas 0.2.3 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.agent.md +54 -0
  3. data/.rubocop.yml +7 -1
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +58 -0
  6. data/DELIVERY_SUMMARY.md +318 -0
  7. data/GITHUB_ACTIONS_QUICK_START.md +110 -0
  8. data/GITHUB_ACTIONS_SETUP.md +313 -0
  9. data/GITHUB_ACTIONS_SUMMARY.md +231 -0
  10. data/LICENSE.txt +1 -1
  11. data/LOCAL_TESTING_SUMMARY.md +30 -13
  12. data/READY_TO_RELEASE.md +308 -0
  13. data/RELEASE_NOTES.md +194 -0
  14. data/RELEASE_STEPS.md +122 -0
  15. data/WORKFLOW_FIXES.md +188 -0
  16. data/docs/index.html +929 -0
  17. data/lib/pg_multitenant_schemas/configuration.rb +7 -1
  18. data/lib/pg_multitenant_schemas/context.rb +92 -3
  19. data/lib/pg_multitenant_schemas/rails/controller_concern.rb +81 -6
  20. data/lib/pg_multitenant_schemas/rails/generators/install_generator.rb +27 -0
  21. data/lib/pg_multitenant_schemas/rails/generators/tenant_migration_generator.rb +30 -0
  22. data/lib/pg_multitenant_schemas/rails/model_concern.rb +3 -3
  23. data/lib/pg_multitenant_schemas/rails/railtie.rb +27 -0
  24. data/lib/pg_multitenant_schemas/schema_switcher.rb +73 -7
  25. data/lib/pg_multitenant_schemas/tenant_resolver.rb +3 -3
  26. data/lib/pg_multitenant_schemas/ui/app/assets/stylesheets/pg_multitenant_schemas/ui/application.css +94 -0
  27. data/lib/pg_multitenant_schemas/ui/app/controllers/pg_multitenant_schemas/ui/tenants_controller.rb +130 -0
  28. data/lib/pg_multitenant_schemas/ui/app/views/layouts/pg_multitenant_schemas/ui/application.html.erb +29 -0
  29. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/index.html.erb +85 -0
  30. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/migration_status.html.erb +45 -0
  31. data/lib/pg_multitenant_schemas/ui/app/views/pg_multitenant_schemas/ui/tenants/new.html.erb +50 -0
  32. data/lib/pg_multitenant_schemas/ui/config/routes.rb +20 -0
  33. data/lib/pg_multitenant_schemas/ui/engine.rb +49 -0
  34. data/lib/pg_multitenant_schemas/ui.rb +9 -0
  35. data/lib/pg_multitenant_schemas/version.rb +1 -1
  36. data/lib/pg_multitenant_schemas.rb +155 -29
  37. data/pg_multitenant_schemas.gemspec +15 -9
  38. data/pre-release-check.sh +46 -0
  39. metadata +39 -12
data/docs/index.html ADDED
@@ -0,0 +1,929 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PgMultitenantSchemas - PostgreSQL Schema-Based Multitenancy for Rails</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ :root {
15
+ --primary: #2563eb;
16
+ --primary-dark: #1e40af;
17
+ --secondary: #7c3aed;
18
+ --success: #16a34a;
19
+ --warning: #ea580c;
20
+ --danger: #dc2626;
21
+ --light: #f8fafc;
22
+ --dark: #0f172a;
23
+ --gray: #64748b;
24
+ --gray-light: #e2e8f0;
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
29
+ line-height: 1.6;
30
+ color: var(--dark);
31
+ background: var(--light);
32
+ }
33
+
34
+ .container {
35
+ max-width: 1200px;
36
+ margin: 0 auto;
37
+ padding: 0 20px;
38
+ }
39
+
40
+ /* Navigation */
41
+ nav {
42
+ background: white;
43
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
44
+ position: sticky;
45
+ top: 0;
46
+ z-index: 100;
47
+ }
48
+
49
+ nav .container {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ padding: 1rem 20px;
54
+ }
55
+
56
+ nav h1 {
57
+ font-size: 1.5rem;
58
+ color: var(--primary);
59
+ font-weight: 700;
60
+ }
61
+
62
+ nav ul {
63
+ display: flex;
64
+ list-style: none;
65
+ gap: 2rem;
66
+ }
67
+
68
+ nav a {
69
+ color: var(--dark);
70
+ text-decoration: none;
71
+ font-weight: 500;
72
+ transition: color 0.3s;
73
+ }
74
+
75
+ nav a:hover {
76
+ color: var(--primary);
77
+ }
78
+
79
+ /* Hero Section */
80
+ .hero {
81
+ background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
82
+ color: white;
83
+ padding: 4rem 0;
84
+ text-align: center;
85
+ }
86
+
87
+ .hero h2 {
88
+ font-size: 2.5rem;
89
+ margin-bottom: 1rem;
90
+ font-weight: 700;
91
+ }
92
+
93
+ .hero p {
94
+ font-size: 1.25rem;
95
+ opacity: 0.95;
96
+ margin-bottom: 2rem;
97
+ }
98
+
99
+ .buttons {
100
+ display: flex;
101
+ gap: 1rem;
102
+ justify-content: center;
103
+ flex-wrap: wrap;
104
+ }
105
+
106
+ .btn {
107
+ padding: 0.75rem 1.5rem;
108
+ border-radius: 0.5rem;
109
+ text-decoration: none;
110
+ font-weight: 600;
111
+ transition: all 0.3s;
112
+ border: none;
113
+ cursor: pointer;
114
+ display: inline-block;
115
+ }
116
+
117
+ .btn-primary {
118
+ background: white;
119
+ color: var(--primary);
120
+ }
121
+
122
+ .btn-primary:hover {
123
+ transform: translateY(-2px);
124
+ box-shadow: 0 10px 20px rgba(0,0,0,0.2);
125
+ }
126
+
127
+ .btn-secondary {
128
+ background: rgba(255,255,255,0.2);
129
+ color: white;
130
+ border: 2px solid white;
131
+ }
132
+
133
+ .btn-secondary:hover {
134
+ background: rgba(255,255,255,0.3);
135
+ }
136
+
137
+ /* Features Section */
138
+ .features {
139
+ padding: 4rem 0;
140
+ }
141
+
142
+ .features h3 {
143
+ text-align: center;
144
+ font-size: 2rem;
145
+ margin-bottom: 3rem;
146
+ color: var(--dark);
147
+ }
148
+
149
+ .feature-grid {
150
+ display: grid;
151
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
152
+ gap: 2rem;
153
+ }
154
+
155
+ .feature-card {
156
+ background: white;
157
+ padding: 2rem;
158
+ border-radius: 0.75rem;
159
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
160
+ transition: all 0.3s;
161
+ }
162
+
163
+ .feature-card:hover {
164
+ transform: translateY(-5px);
165
+ box-shadow: 0 10px 25px rgba(0,0,0,0.15);
166
+ }
167
+
168
+ .feature-icon {
169
+ font-size: 2.5rem;
170
+ margin-bottom: 1rem;
171
+ }
172
+
173
+ .feature-card h4 {
174
+ font-size: 1.25rem;
175
+ margin-bottom: 0.5rem;
176
+ color: var(--primary);
177
+ }
178
+
179
+ .feature-card p {
180
+ color: var(--gray);
181
+ }
182
+
183
+ /* Sections */
184
+ section {
185
+ background: white;
186
+ padding: 3rem 0;
187
+ border-bottom: 1px solid var(--gray-light);
188
+ }
189
+
190
+ section h2 {
191
+ font-size: 2rem;
192
+ margin-bottom: 1.5rem;
193
+ color: var(--dark);
194
+ }
195
+
196
+ section h3 {
197
+ font-size: 1.5rem;
198
+ margin: 2rem 0 1rem 0;
199
+ color: var(--primary);
200
+ }
201
+
202
+ section h4 {
203
+ font-size: 1.1rem;
204
+ margin: 1.5rem 0 0.75rem 0;
205
+ color: var(--dark);
206
+ }
207
+
208
+ section p {
209
+ margin-bottom: 1rem;
210
+ color: var(--gray);
211
+ line-height: 1.8;
212
+ }
213
+
214
+ /* Code Blocks */
215
+ code {
216
+ background: var(--light);
217
+ padding: 0.2rem 0.4rem;
218
+ border-radius: 0.25rem;
219
+ font-family: 'Monaco', 'Courier New', monospace;
220
+ font-size: 0.9em;
221
+ color: var(--danger);
222
+ }
223
+
224
+ pre {
225
+ background: var(--dark);
226
+ color: #e2e8f0;
227
+ padding: 1.5rem;
228
+ border-radius: 0.5rem;
229
+ overflow-x: auto;
230
+ margin: 1rem 0;
231
+ font-size: 0.9rem;
232
+ line-height: 1.5;
233
+ }
234
+
235
+ pre code {
236
+ background: none;
237
+ padding: 0;
238
+ color: inherit;
239
+ }
240
+
241
+ /* API Reference */
242
+ .api-item {
243
+ background: var(--light);
244
+ padding: 1.5rem;
245
+ border-radius: 0.5rem;
246
+ margin-bottom: 1.5rem;
247
+ border-left: 4px solid var(--primary);
248
+ }
249
+
250
+ .api-item .signature {
251
+ font-family: 'Monaco', 'Courier New', monospace;
252
+ background: white;
253
+ padding: 1rem;
254
+ border-radius: 0.25rem;
255
+ overflow-x: auto;
256
+ margin-bottom: 1rem;
257
+ font-size: 0.85rem;
258
+ }
259
+
260
+ .api-item h4 {
261
+ color: var(--primary);
262
+ }
263
+
264
+ .params {
265
+ margin: 1rem 0;
266
+ }
267
+
268
+ .params h5 {
269
+ font-size: 0.95rem;
270
+ color: var(--dark);
271
+ margin: 0.75rem 0 0.5rem 0;
272
+ font-weight: 600;
273
+ }
274
+
275
+ .param {
276
+ margin-left: 1.5rem;
277
+ padding: 0.5rem 0;
278
+ border-bottom: 1px solid var(--gray-light);
279
+ }
280
+
281
+ .param .name {
282
+ font-weight: 600;
283
+ color: var(--dark);
284
+ font-family: 'Monaco', 'Courier New', monospace;
285
+ }
286
+
287
+ .param .type {
288
+ color: var(--secondary);
289
+ font-family: 'Monaco', 'Courier New', monospace;
290
+ font-size: 0.9em;
291
+ }
292
+
293
+ .param .description {
294
+ color: var(--gray);
295
+ margin-top: 0.25rem;
296
+ }
297
+
298
+ /* Tables */
299
+ table {
300
+ width: 100%;
301
+ border-collapse: collapse;
302
+ margin: 1.5rem 0;
303
+ }
304
+
305
+ th, td {
306
+ padding: 1rem;
307
+ text-align: left;
308
+ border-bottom: 1px solid var(--gray-light);
309
+ }
310
+
311
+ th {
312
+ background: var(--light);
313
+ font-weight: 600;
314
+ color: var(--dark);
315
+ }
316
+
317
+ tr:hover {
318
+ background: var(--light);
319
+ }
320
+
321
+ /* Links */
322
+ a {
323
+ color: var(--primary);
324
+ text-decoration: none;
325
+ font-weight: 500;
326
+ }
327
+
328
+ a:hover {
329
+ text-decoration: underline;
330
+ }
331
+
332
+ /* Footer */
333
+ footer {
334
+ background: var(--dark);
335
+ color: white;
336
+ padding: 2rem 0;
337
+ text-align: center;
338
+ }
339
+
340
+ footer a {
341
+ color: #60a5fa;
342
+ }
343
+
344
+ /* Tabs */
345
+ .tabs {
346
+ display: flex;
347
+ border-bottom: 2px solid var(--gray-light);
348
+ margin-bottom: 1.5rem;
349
+ gap: 1rem;
350
+ }
351
+
352
+ .tab-btn {
353
+ padding: 0.75rem 1.5rem;
354
+ background: none;
355
+ border: none;
356
+ cursor: pointer;
357
+ font-weight: 600;
358
+ color: var(--gray);
359
+ border-bottom: 2px solid transparent;
360
+ transition: all 0.3s;
361
+ }
362
+
363
+ .tab-btn.active {
364
+ color: var(--primary);
365
+ border-bottom-color: var(--primary);
366
+ }
367
+
368
+ .tab-btn:hover {
369
+ color: var(--primary);
370
+ }
371
+
372
+ .tab-content {
373
+ display: none;
374
+ }
375
+
376
+ .tab-content.active {
377
+ display: block;
378
+ }
379
+
380
+ /* Badges */
381
+ .badge {
382
+ display: inline-block;
383
+ padding: 0.25rem 0.75rem;
384
+ border-radius: 0.25rem;
385
+ font-size: 0.85rem;
386
+ font-weight: 600;
387
+ margin-right: 0.5rem;
388
+ }
389
+
390
+ .badge-primary {
391
+ background: #dbeafe;
392
+ color: var(--primary);
393
+ }
394
+
395
+ .badge-success {
396
+ background: #dcfce7;
397
+ color: var(--success);
398
+ }
399
+
400
+ .badge-warning {
401
+ background: #fed7aa;
402
+ color: var(--warning);
403
+ }
404
+
405
+ /* Responsive */
406
+ @media (max-width: 768px) {
407
+ nav ul {
408
+ gap: 1rem;
409
+ }
410
+
411
+ .hero h2 {
412
+ font-size: 1.75rem;
413
+ }
414
+
415
+ .hero p {
416
+ font-size: 1rem;
417
+ }
418
+
419
+ .buttons {
420
+ flex-direction: column;
421
+ align-items: center;
422
+ }
423
+
424
+ section h2 {
425
+ font-size: 1.5rem;
426
+ }
427
+ }
428
+ </style>
429
+ </head>
430
+ <body>
431
+ <!-- Navigation -->
432
+ <nav>
433
+ <div class="container" style="display: flex; justify-content: space-between; align-items: center;">
434
+ <h1>📦 PgMultitenantSchemas</h1>
435
+ <ul style="display: flex; gap: 2rem; list-style: none;">
436
+ <li><a href="#features">Features</a></li>
437
+ <li><a href="#installation">Install</a></li>
438
+ <li><a href="#api">API</a></li>
439
+ <li><a href="#examples">Examples</a></li>
440
+ <li><a href="#docs">Docs</a></li>
441
+ </ul>
442
+ </div>
443
+ </nav>
444
+
445
+ <!-- Hero Section -->
446
+ <section class="hero">
447
+ <div class="container">
448
+ <h2>PostgreSQL Schema-Based Multitenancy</h2>
449
+ <p>Modern Rails gem for secure, isolated, high-performance multi-tenant applications</p>
450
+ <div class="buttons">
451
+ <a href="#installation" class="btn btn-primary">Get Started</a>
452
+ <a href="https://github.com/rubenpazch/pg_multitenant_schemas" class="btn btn-secondary">View on GitHub</a>
453
+ </div>
454
+ </div>
455
+ </section>
456
+
457
+ <!-- Features Section -->
458
+ <section id="features">
459
+ <div class="container">
460
+ <h3>Powerful Features</h3>
461
+ <div class="feature-grid">
462
+ <div class="feature-card">
463
+ <div class="feature-icon">🏢</div>
464
+ <h4>Schema Isolation</h4>
465
+ <p>Each tenant gets their own PostgreSQL schema. Complete data isolation with shared application code.</p>
466
+ </div>
467
+ <div class="feature-card">
468
+ <div class="feature-icon">🔄</div>
469
+ <h4>Automatic Switching</h4>
470
+ <p>Seamlessly switch between tenant schemas. Context is maintained throughout the request lifecycle.</p>
471
+ </div>
472
+ <div class="feature-card">
473
+ <div class="feature-icon">🌐</div>
474
+ <h4>Subdomain Resolution</h4>
475
+ <p>Extract tenant from request subdomains. Built-in tenant identification strategies.</p>
476
+ </div>
477
+ <div class="feature-card">
478
+ <div class="feature-icon">🧵</div>
479
+ <h4>Thread-Safe</h4>
480
+ <p>Safe for concurrent operations. Proper context isolation for multi-threaded environments.</p>
481
+ </div>
482
+ <div class="feature-card">
483
+ <div class="feature-icon">⚡</div>
484
+ <h4>High Performance</h4>
485
+ <p>Minimal overhead. Optimized for Rails 8+ with efficient schema switching.</p>
486
+ </div>
487
+ <div class="feature-card">
488
+ <div class="feature-icon">🔒</div>
489
+ <h4>Security-First</h4>
490
+ <p>Database-level tenant isolation. No risk of accidental cross-tenant queries.</p>
491
+ </div>
492
+ </div>
493
+ </div>
494
+ </section>
495
+
496
+ <!-- Installation Section -->
497
+ <section id="installation">
498
+ <div class="container">
499
+ <h2>Installation</h2>
500
+
501
+ <h3>Requirements</h3>
502
+ <ul style="margin: 1rem 0 1rem 2rem; color: var(--gray);">
503
+ <li>Ruby 3.0+</li>
504
+ <li>Rails 7.0+ (optimized for Rails 8)</li>
505
+ <li>PostgreSQL 12+</li>
506
+ </ul>
507
+
508
+ <h3>Setup</h3>
509
+ <p>Add to your Gemfile:</p>
510
+ <pre><code>gem 'pg_multitenant_schemas'
511
+ bundle install</code></pre>
512
+
513
+ <h3>Configuration</h3>
514
+ <p>Create <code>config/initializers/pg_multitenant_schemas.rb</code>:</p>
515
+ <pre><code>PgMultitenantSchemas.configure do |config|
516
+ config.connection_class = 'ApplicationRecord'
517
+ config.tenant_model_class = 'Tenant'
518
+ config.default_schema = 'public'
519
+ config.excluded_subdomains = ['www', 'api', 'admin']
520
+ config.development_fallback = true
521
+ config.auto_create_schemas = true
522
+ end</code></pre>
523
+ </div>
524
+ </section>
525
+
526
+ <!-- Quick Start -->
527
+ <section>
528
+ <div class="container">
529
+ <h2>Quick Start</h2>
530
+
531
+ <h3>Migrate All Tenants</h3>
532
+ <pre><code># Migrate all tenant schemas at once
533
+ rails tenants:migrate
534
+
535
+ # Migrate a specific tenant
536
+ rails tenants:migrate_tenant[acme_corp]
537
+
538
+ # Check migration status
539
+ rails tenants:status</code></pre>
540
+
541
+ <h3>Create New Tenant</h3>
542
+ <pre><code># Create tenant with setup (schema + migrations)
543
+ rails tenants:create[new_tenant]
544
+
545
+ # Create with attributes
546
+ rails tenants:new['{"subdomain":"acme","name":"ACME Corp"}']</code></pre>
547
+
548
+ <h3>Context Management</h3>
549
+ <pre><code>// Switch to tenant context
550
+ PgMultitenantSchemas.switch_to_tenant(tenant)
551
+
552
+ # Use with block
553
+ PgMultitenantSchemas.with_tenant(tenant) do
554
+ User.all # Queries tenant's schema
555
+ end
556
+
557
+ # Get current context
558
+ PgMultitenantSchemas.current_schema #=> "tenant_123"</code></pre>
559
+
560
+ <h3>Rails Integration</h3>
561
+ <pre><code>class ApplicationController < ActionController::Base
562
+ include PgMultitenantSchemas::Rails::ControllerConcern
563
+
564
+ before_action :resolve_tenant
565
+
566
+ private
567
+
568
+ def resolve_tenant
569
+ tenant = resolve_tenant_from_subdomain
570
+ switch_to_tenant(tenant) if tenant
571
+ end
572
+ end</code></pre>
573
+ </div>
574
+ </section>
575
+
576
+ <!-- API Reference Section -->
577
+ <section id="api">
578
+ <div class="container">
579
+ <h2>API Reference</h2>
580
+
581
+ <h3>Context Class</h3>
582
+
583
+ <div class="api-item">
584
+ <h4>current_schema</h4>
585
+ <div class="signature">PgMultitenantSchemas::Context.current_schema → String</div>
586
+ <p>Get the current tenant schema name. Returns default schema if not set.</p>
587
+ <div class="params">
588
+ <h5>Returns</h5>
589
+ <div class="param">
590
+ <span class="type">String</span>
591
+ <span class="description">The current schema name</span>
592
+ </div>
593
+ </div>
594
+ <pre><code>PgMultitenantSchemas::Context.current_schema
595
+ #=> "tenant_123"</code></pre>
596
+ </div>
597
+
598
+ <div class="api-item">
599
+ <h4>current_tenant</h4>
600
+ <div class="signature">PgMultitenantSchemas::Context.current_tenant → Object</div>
601
+ <p>Get the current tenant object.</p>
602
+ <pre><code>PgMultitenantSchemas::Context.current_tenant
603
+ #=> #&lt;Tenant id=1, subdomain="acme"&gt;</code></pre>
604
+ </div>
605
+
606
+ <div class="api-item">
607
+ <h4>switch_to_tenant(tenant)</h4>
608
+ <div class="signature">PgMultitenantSchemas::Context.switch_to_tenant(tenant) → void</div>
609
+ <p>Switch to the given tenant's schema.</p>
610
+ <div class="params">
611
+ <h5>Parameters</h5>
612
+ <div class="param">
613
+ <span class="name">tenant</span>
614
+ <span class="type">Tenant | String</span>
615
+ <span class="description">Tenant object or schema name</span>
616
+ </div>
617
+ </div>
618
+ <pre><code>PgMultitenantSchemas::Context.switch_to_tenant(tenant)
619
+ # Now all queries use tenant's schema</code></pre>
620
+ </div>
621
+
622
+ <div class="api-item">
623
+ <h4>with_tenant(tenant) { block }</h4>
624
+ <div class="signature">PgMultitenantSchemas::Context.with_tenant(tenant) { |block| } → Object</div>
625
+ <p>Execute block within tenant context. Restores previous context after block.</p>
626
+ <div class="params">
627
+ <h5>Parameters</h5>
628
+ <div class="param">
629
+ <span class="name">tenant</span>
630
+ <span class="type">Tenant | String</span>
631
+ <span class="description">Tenant object or schema name</span>
632
+ </div>
633
+ </div>
634
+ <pre><code>PgMultitenantSchemas::Context.with_tenant(tenant) do
635
+ User.all # Queries tenant's schema
636
+ Order.create!(...)
637
+ end
638
+ # Context restored here</code></pre>
639
+ </div>
640
+
641
+ <h3>SchemaSwitcher Class</h3>
642
+
643
+ <div class="api-item">
644
+ <h4>create_schema(schema_name)</h4>
645
+ <div class="signature">PgMultitenantSchemas::SchemaSwitcher.create_schema(schema_name) → void</div>
646
+ <p>Create a new PostgreSQL schema.</p>
647
+ <div class="params">
648
+ <h5>Parameters</h5>
649
+ <div class="param">
650
+ <span class="name">schema_name</span>
651
+ <span class="type">String</span>
652
+ <span class="description">Name of the schema to create</span>
653
+ </div>
654
+ </div>
655
+ </div>
656
+
657
+ <div class="api-item">
658
+ <h4>drop_schema(schema_name, cascade: true)</h4>
659
+ <div class="signature">PgMultitenantSchemas::SchemaSwitcher.drop_schema(schema_name, cascade: true) → void</div>
660
+ <p>Drop a PostgreSQL schema.</p>
661
+ <div class="params">
662
+ <h5>Parameters</h5>
663
+ <div class="param">
664
+ <span class="name">schema_name</span>
665
+ <span class="type">String</span>
666
+ <span class="description">Name of the schema to drop</span>
667
+ </div>
668
+ <div class="param">
669
+ <span class="name">cascade</span>
670
+ <span class="type">Boolean</span>
671
+ <span class="description">Drop dependent objects (default: true)</span>
672
+ </div>
673
+ </div>
674
+ </div>
675
+
676
+ <div class="api-item">
677
+ <h4>schema_exists?(schema_name)</h4>
678
+ <div class="signature">PgMultitenantSchemas::SchemaSwitcher.schema_exists?(schema_name) → Boolean</div>
679
+ <p>Check if a schema exists in the database.</p>
680
+ <div class="params">
681
+ <h5>Parameters</h5>
682
+ <div class="param">
683
+ <span class="name">schema_name</span>
684
+ <span class="type">String</span>
685
+ <span class="description">Name of the schema to check</span>
686
+ </div>
687
+ </div>
688
+ <pre><code>if PgMultitenantSchemas::SchemaSwitcher.schema_exists?('tenant_123')
689
+ # Schema exists
690
+ end</code></pre>
691
+ </div>
692
+
693
+ <div class="api-item">
694
+ <h4>list_schemas</h4>
695
+ <div class="signature">PgMultitenantSchemas::SchemaSwitcher.list_schemas → Array[String]</div>
696
+ <p>List all schemas in the database.</p>
697
+ <pre><code>PgMultitenantSchemas::SchemaSwitcher.list_schemas
698
+ #=> ["public", "tenant_123", "tenant_456"]</code></pre>
699
+ </div>
700
+
701
+ <h3>Migrator Class</h3>
702
+
703
+ <div class="api-item">
704
+ <h4>migrate_all</h4>
705
+ <div class="signature">PgMultitenantSchemas::Migrator.migrate_all → Hash</div>
706
+ <p>Run migrations across all tenant schemas.</p>
707
+ <pre><code>results = PgMultitenantSchemas::Migrator.migrate_all
708
+ #=> {
709
+ # success: ["tenant_1", "tenant_2"],
710
+ # failed: [],
711
+ # total_migrations: 5
712
+ # }</code></pre>
713
+ </div>
714
+
715
+ <div class="api-item">
716
+ <h4>setup_tenant(schema_name)</h4>
717
+ <div class="signature">PgMultitenantSchemas::Migrator.setup_tenant(schema_name) → Boolean</div>
718
+ <p>Create schema and run migrations for a new tenant.</p>
719
+ <div class="params">
720
+ <h5>Parameters</h5>
721
+ <div class="param">
722
+ <span class="name">schema_name</span>
723
+ <span class="type">String</span>
724
+ <span class="description">Name of the tenant schema</span>
725
+ </div>
726
+ </div>
727
+ </div>
728
+
729
+ <div class="api-item">
730
+ <h4>migration_status</h4>
731
+ <div class="signature">PgMultitenantSchemas::Migrator.migration_status → Hash</div>
732
+ <p>Get migration status for all tenants.</p>
733
+ <pre><code>PgMultitenantSchemas::Migrator.migration_status
734
+ #=> {
735
+ # "tenant_1" => { status: :up_to_date, pending: 0 },
736
+ # "tenant_2" => { status: :pending, pending: 2 }
737
+ # }</code></pre>
738
+ </div>
739
+ </div>
740
+ </section>
741
+
742
+ <!-- Examples Section -->
743
+ <section id="examples">
744
+ <div class="container">
745
+ <h2>Common Patterns</h2>
746
+
747
+ <h3>Subdomain-Based Routing</h3>
748
+ <pre><code>class ApplicationController < ActionController::Base
749
+ include PgMultitenantSchemas::Rails::ControllerConcern
750
+
751
+ before_action :authenticate_user!
752
+ before_action :resolve_tenant_from_subdomain
753
+ before_action :ensure_user_in_tenant
754
+
755
+ private
756
+
757
+ def resolve_tenant_from_subdomain
758
+ @tenant = resolve_tenant_from_request(request)
759
+ switch_to_tenant(@tenant) if @tenant
760
+ end
761
+
762
+ def ensure_user_in_tenant
763
+ redirect_unless current_user.tenant == @tenant
764
+ end
765
+ end</code></pre>
766
+
767
+ <h3>Background Jobs with Tenant Context</h3>
768
+ <pre><code>class ProcessOrderJob
769
+ include Sidekiq::Worker
770
+
771
+ def perform(order_id, tenant_id)
772
+ tenant = Tenant.find(tenant_id)
773
+
774
+ PgMultitenantSchemas.with_tenant(tenant) do
775
+ order = Order.find(order_id)
776
+ # Process order in tenant's schema
777
+ order.process!
778
+ end
779
+ end
780
+ end
781
+
782
+ # Enqueue with tenant context
783
+ tenant = Tenant.first
784
+ ProcessOrderJob.perform_async(order.id, tenant.id)</code></pre>
785
+
786
+ <h3>Cross-Tenant Admin Operations</h3>
787
+ <pre><code>class AdminController < ApplicationController
788
+ def platform_stats
789
+ stats = {}
790
+
791
+ Tenant.find_each do |tenant|
792
+ PgMultitenantSchemas.with_tenant(tenant) do
793
+ stats[tenant.subdomain] = {
794
+ users_count: User.count,
795
+ orders_count: Order.count,
796
+ revenue: Order.sum(:amount)
797
+ }
798
+ end
799
+ end
800
+
801
+ render json: stats
802
+ end
803
+ end</code></pre>
804
+
805
+ <h3>Testing with Tenants</h3>
806
+ <pre><code>RSpec.describe User, type: :model do
807
+ let(:tenant) { create(:tenant) }
808
+
809
+ before { PgMultitenantSchemas.switch_to_tenant(tenant) }
810
+ after { PgMultitenantSchemas.reset! }
811
+
812
+ it 'creates user in tenant schema' do
813
+ user = create(:user)
814
+
815
+ PgMultitenantSchemas.with_tenant(tenant) do
816
+ expect(User.find(user.id)).to eq(user)
817
+ end
818
+ end
819
+ end</code></pre>
820
+ </div>
821
+ </section>
822
+
823
+ <!-- Documentation Section -->
824
+ <section id="docs">
825
+ <div class="container">
826
+ <h2>Complete Documentation</h2>
827
+
828
+ <h3>Core Components</h3>
829
+ <table>
830
+ <thead>
831
+ <tr>
832
+ <th>Component</th>
833
+ <th>Purpose</th>
834
+ <th>Link</th>
835
+ </tr>
836
+ </thead>
837
+ <tbody>
838
+ <tr>
839
+ <td><strong>Context</strong></td>
840
+ <td>Thread-safe tenant context management</td>
841
+ <td><a href="context.md">Documentation</a></td>
842
+ </tr>
843
+ <tr>
844
+ <td><strong>SchemaSwitcher</strong></td>
845
+ <td>Low-level PostgreSQL schema operations</td>
846
+ <td><a href="schema_switcher.md">Documentation</a></td>
847
+ </tr>
848
+ <tr>
849
+ <td><strong>Migrator</strong></td>
850
+ <td>Automated migration management</td>
851
+ <td><a href="migrator.md">Documentation</a></td>
852
+ </tr>
853
+ <tr>
854
+ <td><strong>TenantResolver</strong></td>
855
+ <td>Tenant identification strategies</td>
856
+ <td><a href="tenant_resolver.md">Documentation</a></td>
857
+ </tr>
858
+ <tr>
859
+ <td><strong>Configuration</strong></td>
860
+ <td>Gem configuration and settings</td>
861
+ <td><a href="configuration.md">Documentation</a></td>
862
+ </tr>
863
+ <tr>
864
+ <td><strong>Rails Integration</strong></td>
865
+ <td>Framework components and patterns</td>
866
+ <td><a href="rails_integration.md">Documentation</a></td>
867
+ </tr>
868
+ </tbody>
869
+ </table>
870
+
871
+ <h3>Guides</h3>
872
+ <ul style="margin: 1rem 0 1rem 2rem; color: var(--gray);">
873
+ <li><a href="testing.md">Testing Guide</a> - RSpec test suite and testing patterns</li>
874
+ <li><a href="integration_testing.md">Integration Testing</a> - PostgreSQL integration testing</li>
875
+ <li><a href="errors.md">Error Handling</a> - Exception classes and error management</li>
876
+ <li><a href="github_actions_setup.md">CI/CD Setup</a> - GitHub Actions automation</li>
877
+ </ul>
878
+
879
+ <h3>Troubleshooting</h3>
880
+ <ul style="margin: 1rem 0 1rem 2rem; color: var(--gray);">
881
+ <li><a href="github_actions_permissions_fix.md">GitHub Actions Permissions</a></li>
882
+ <li><a href="local_workflow_testing.md">Local Workflow Testing</a></li>
883
+ </ul>
884
+ </div>
885
+ </section>
886
+
887
+ <!-- Stats Section -->
888
+ <section style="background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); color: white;">
889
+ <div class="container" style="text-align: center;">
890
+ <h2 style="color: white; margin-bottom: 2rem;">Production Ready</h2>
891
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 2rem; text-align: center;">
892
+ <div>
893
+ <div style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">217</div>
894
+ <div style="font-size: 1.1rem; opacity: 0.95;">Test Examples</div>
895
+ </div>
896
+ <div>
897
+ <div style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">100%</div>
898
+ <div style="font-size: 1.1rem; opacity: 0.95;">Tests Passing</div>
899
+ </div>
900
+ <div>
901
+ <div style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">0</div>
902
+ <div style="font-size: 1.1rem; opacity: 0.95;">CVE Issues</div>
903
+ </div>
904
+ <div>
905
+ <div style="font-size: 2.5rem; font-weight: 700; margin-bottom: 0.5rem;">Rails 8</div>
906
+ <div style="font-size: 1.1rem; opacity: 0.95;">Optimized</div>
907
+ </div>
908
+ </div>
909
+ </div>
910
+ </section>
911
+
912
+ <!-- Footer -->
913
+ <footer>
914
+ <div class="container">
915
+ <p style="margin-bottom: 1rem;">
916
+ <strong>PgMultitenantSchemas</strong> • Modern PostgreSQL schema-based multitenancy for Rails
917
+ </p>
918
+ <p>
919
+ <a href="https://github.com/rubenpazch/pg_multitenant_schemas">GitHub</a> •
920
+ <a href="https://rubygems.org/gems/pg_multitenant_schemas">RubyGems</a> •
921
+ <a href="LICENSE.txt">MIT License</a>
922
+ </p>
923
+ <p style="margin-top: 1.5rem; opacity: 0.8;">
924
+ © 2025 L BYTE EIRL. Built with ❤️ for the Rails community.
925
+ </p>
926
+ </div>
927
+ </footer>
928
+ </body>
929
+ </html>