Kirayedar
View SourceMulti-tenancy library for Elixir/Phoenix with schema-based isolation.
Features
- ðĒ Schema-based isolation using PostgreSQL schemas or MySQL databases
- ð Intelligent tenant resolution from host/subdomain
- ð Plug integration for automatic tenant context
- ð Migration helpers for multi-tenant databases
- ð Telemetry support for monitoring and observability
- ðŠķ Lightweight with minimal dependencies
- ð Observable with comprehensive structured logging
- ð Dynamic adapter detection from your Repo configuration
- ð ïļ Mix tasks for easy setup and code generation
Installation
Add kirayedar to your list of dependencies in mix.exs:
def deps do
[
{:kirayedar, "~> 0.1.0"},
# Choose your database adapter
{:postgrex, ">= 0.0.0"}, # For PostgreSQL
# {:myxql, ">= 0.0.0"}, # For MySQL
]
endQuick Start with Mix Tasks
1. Setup Kirayedar
Run the interactive setup task:
mix kirayedar.setup
This will:
- Prompt you for configuration options
- Generate the tenant model
- Create the tenant table migration
- Update your
config/config.exs - Optionally generate LiveView CRUD interfaces
Example session:
What do you want to call your tenant/organization? [Tenant]: Organization
Do you want to use binary_id (UUID)? [Yn]: Y
What is your Admin Host? [localhost]: admin.myapp.com
What is your primary domain? [example.com]: myapp.com
Do you want to generate LiveViews for CRUD? [Yn]: Y2. Run Migrations
mix ecto.migrate
3. Add Plug to Your Endpoint
Edit lib/my_app_web/endpoint.ex:
defmodule MyAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :my_app
# Add this line
plug Kirayedar.Plug
# ... rest of your plugs
end4. Update Your Repo
Edit lib/my_app/repo.ex:
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
# Add this line
use Kirayedar.Repo
end5. Create Your First Tenant
iex> Kirayedar.create(MyApp.Repo, "acme_corp")
:ok
iex> Kirayedar.Migration.migrate(MyApp.Repo, "acme_corp")
:okManual Configuration
If you prefer manual setup, configure in config/config.exs:
config :kirayedar,
repo: MyApp.Repo,
primary_domain: "example.com",
admin_host: "admin.example.com",
tenant_model: MyApp.Accounts.Organization
# adapter: :postgres # Optional - auto-detected from RepoUsage
Creating and Managing Tenants
# Create a tenant schema
Kirayedar.create(MyApp.Repo, "acme_corp")
# Run migrations for the tenant
Kirayedar.Migration.migrate(MyApp.Repo, "acme_corp")
# Drop a tenant schema
Kirayedar.drop(MyApp.Repo, "acme_corp")
# Rollback migrations
Kirayedar.Migration.rollback(MyApp.Repo, "acme_corp", step: 1)Migrating All Tenants
# In your release tasks or deployment scripts
defmodule MyApp.ReleaseTasks do
def migrate_all do
{:ok, _} = Application.ensure_all_started(:kirayedar)
Kirayedar.Migration.migrate_all(
MyApp.Repo,
MyApp.Accounts.Organization
)
end
endGlobal Scope
Query global tables while in a tenant context:
# Inside a tenant request
Kirayedar.scope_global(fn ->
Repo.all(GlobalSettings)
end)Manual Tenant Context
# Set tenant manually
Kirayedar.put_tenant("acme_corp")
Repo.all(Post) # Queries acme_corp schema
# Execute in specific tenant context
Kirayedar.with_tenant("acme_corp", fn ->
Repo.all(Post)
end)Telemetry Integration
Monitor tenant operations with Telemetry:
# In your application.ex
defmodule MyApp.Application do
def start(_type, _args) do
:telemetry.attach_many(
"kirayedar-handler",
[
[:kirayedar, :tenant, :create],
[:kirayedar, :tenant, :drop],
[:kirayedar, :tenant, :migrate],
[:kirayedar, :tenant, :create, :error],
[:kirayedar, :tenant, :drop, :error],
[:kirayedar, :tenant, :migrate, :error]
],
&MyApp.TelemetryHandler.handle_event/4,
nil
)
# ... rest of your supervision tree
end
end
defmodule MyApp.TelemetryHandler do
require Logger
def handle_event([:kirayedar, :tenant, action], measurements, metadata, _config) do
Logger.info("Tenant operation completed",
action: action,
tenant: metadata.tenant,
duration_ms: measurements.duration
)
end
def handle_event([:kirayedar, :tenant, action, :error], _measurements, metadata, _config) do
Logger.error("Tenant operation failed",
action: action,
tenant: metadata.tenant,
error: inspect(metadata.error)
)
end
endTenant Resolution
Kirayedar resolves tenants in the following priority order:
- Admin host check - Returns
nilfor admin domain - Exact domain match - Checks
domainfield in tenant model - Subdomain extraction - Extracts subdomain from primary domain
- Slug fallback - Checks
slugfield for custom domains
Examples:
admin.example.com â nil (admin host)
acme.example.com â "acme" (subdomain)
custom-domain.com â looks up by domain/slug in DB
acme.example.com:4000 â "acme" (port stripped)Testing
Running Tests
# Run all tests
mix test
# Run PostgreSQL tests only
mix test test/kirayedar_postgres_test.exs
# Run MySQL tests only (requires MySQL)
mix test test/kirayedar_mysql_test.exs
# Run with coverage
mix test --cover
Test Database Setup
PostgreSQL
# Create test database
createdb kirayedar_test
# Or using psql
psql -U postgres -c "CREATE DATABASE kirayedar_test;"
MySQL
# Create test database
mysql -u root -p -e "CREATE DATABASE kirayedar_test;"
Test Configuration
Update config/test.exs:
# PostgreSQL
config :kirayedar, Kirayedar.Test.PostgresRepo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "kirayedar_test",
pool: Ecto.Adapters.SQL.Sandbox
# MySQL (optional)
config :kirayedar, Kirayedar.Test.MySQLRepo,
username: "root",
password: "root",
hostname: "localhost",
database: "kirayedar_test",
pool: Ecto.Adapters.SQL.SandboxWriting Tests
defmodule MyApp.TenantTest do
use MyApp.DataCase
test "tenant isolation works" do
Kirayedar.create(Repo, "tenant1")
Kirayedar.create(Repo, "tenant2")
# Insert data in tenant1
Kirayedar.with_tenant("tenant1", fn ->
%Post{title: "Tenant 1 Post"} |> Repo.insert!()
end)
# Verify isolation
count = Kirayedar.with_tenant("tenant2", fn ->
Repo.aggregate(Post, :count)
end)
assert count == 0
end
endMix Tasks Reference
mix kirayedar.setup
Interactive setup wizard that generates:
- Tenant model with customizable name
- Migration file
- Configuration updates
- Optional LiveView CRUD
mix kirayedar.gen.live
Generates LiveView components for tenant management:
- Index view with listing
- Form component for create/update
- Show view for details
Requires prior mix kirayedar.setup.
Production Considerations
1. Connection Pooling
Each tenant schema uses the same connection pool, but queries include the prefix. Monitor your pool size:
config :my_app, MyApp.Repo,
pool_size: 20, # Adjust based on tenant count and load
queue_target: 50002. Migration Strategy
For production deployments:
# In your release module
def migrate do
# Migrate global tables first
Ecto.Migrator.run(MyApp.Repo, :up, all: true)
# Then migrate all tenants
Kirayedar.Migration.migrate_all(MyApp.Repo, MyApp.Accounts.Organization)
end3. Monitoring
Use telemetry to track:
- Migration durations
- Tenant creation/deletion
- Schema switching overhead
- Failed operations
4. Backup Strategy
For PostgreSQL:
# Backup all schemas
pg_dump -U postgres -d myapp_db -n "tenant_*" > tenant_backups.sql
# Backup specific tenant
pg_dump -U postgres -d myapp_db -n "acme_corp" > acme_corp_backup.sql
For MySQL:
# Backup specific tenant database
mysqldump -u root -p acme_corp > acme_corp_backup.sql
Structured Logging
Kirayedar uses structured logging with keyword lists:
# Logs appear as:
[info] Kirayedar: create tenant schema/database tenant=acme_corp
[info] Kirayedar.Resolver: Subdomain match host=acme.example.com tenant=acme
[info] Kirayedar.Migration: Running migrations tenant=acme_corp duration_ms=1234Works seamlessly with:
- Datadog
- CloudWatch
- Loki
- ElasticSearch
License
Apache 2.0
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run tests (
mix test) - Commit your changes (
git commit -am 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Support
- GitHub Issues: https://github.com/viveksingh0143/elixir-kirayedar/issues
- Documentation: https://hexdocs.pm/kirayedar