multi-tenancy-template
Spider-Gazelle Multitenancy Starter
Production-ready Spider-Gazelle template with PostgreSQL, multi-tenant organizations, and OAuth authentication.
Features
- PostgreSQL with migrations
- Multi-tenant organizations with permissions (Admin, Manager, User, Viewer)
- User groups with group-based permissions
- Authentication: username/password, Google OAuth, Microsoft OAuth
- OAuth token storage and refresh
- Organization and group invites with email notifications
- Password reset via email
- Domain mapping
- Docker support
Quick Start
shards install
cp .env.example .env
# Edit .env with your PG_DATABASE_URL
crystal run src/app.cr
# Visit http://localhost:3000
Web UI
The template includes ready-to-use web pages:
/auth/login- Login page (password + OAuth)/auth/forgot-password- Password reset request/organizations- Organizations list and creation/organizations/:id/manage- Organization member management/organizations/:id/groups- Groups management
Authentication
Username/Password
user = Models::User.new(name: "John", email: "john@example.com")
user.password = "secure_password"
user.save!
Or use crystal run create_test_user.cr
OAuth Setup
Google:
- Create OAuth credentials at Google Cloud Console
- Add redirect URI:
http://localhost:3000/auth/oauth/google/callback - Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETin.env
Microsoft:
- Register app at Azure Portal
- Add redirect URI:
http://localhost:3000/auth/oauth/microsoft/callback - Set
MICROSOFT_CLIENT_IDandMICROSOFT_CLIENT_SECRETin.env
API Documentation
Generate OpenAPI docs:
crystal run src/app.cr -- --docs -f openapi.yml
Usage in Controllers
class MyController < App::Base
base "/organizations"
@[AC::Route::Filter(:before_action)]
private def authenticate
require_auth!
end
@[AC::Route::Filter(:before_action)]
private def find_organization(id : String)
@current_org = Models::Organization.find!(UUID.new(id))
end
getter! current_org : Models::Organization
@[AC::Route::Filter(:before_action)]
private def require_admin
require_permission!(current_org, Permissions::Admin)
end
@[AC::Route::GET("/:id/resources")]
def index : Array(Models::Resource)
Models::Resource.where(organization_id: current_org.id).to_a
end
end
Permission Levels
- Admin - Full control
- Manager - Manage members and resources
- User - Create and manage own resources
- Viewer - Read-only access
Database Schema
users- User accounts with password hashauth- OAuth provider linkages and tokensorganizations- Tenant organizations with subdomain and admin grouporganization_users- Membership with permissionsorganization_invites- Pending invitations with email notificationsgroups- User groups within organizations with permission levelsgroup_users- Group membership (with group admin flag)group_invites- Pending group invitationspassword_reset_tokens- Secure password reset tokensdomains- Custom domain mappingsapi_keys- API key authentication with scopesaudit_logs- Activity audit trail
Health Check
Health check endpoints for container orchestration:
GET /health- Basic health statusGET /health/live- Liveness probe (app is running)GET /health/ready- Readiness probe (database connectivity)
API Key Authentication
API keys provide programmatic access with scoped permissions:
# Create API key for a user
api_key, raw_key = Models::ApiKey.create_for_user(
user,
"My API Key",
scopes: ["read", "write"],
expires_at: Time.utc + 30.days
)
# raw_key is only shown once: "sk_..."
# Authenticate via Authorization header
# Authorization: Bearer sk_...
Audit Logging
Key actions are automatically logged to audit_logs:
- User login/logout
- Organization create/update/delete
- Member add/remove
- Invite creation
Access logs in your code:
audit_log(
Models::AuditLog::Actions::CREATE,
Models::AuditLog::Resources::ORGANIZATION,
resource_id: org.id,
organization: org
)
Groups
Groups allow organizing users within an organization with specific permission levels.
Key Features
- Each organization has an automatic "Administrators" group
- Groups have a permission level (Admin, Manager, User, Viewer)
- Users can belong to multiple groups
- Group admins can manage group membership
- Organization admins can manage all groups
- Invite users to groups via email (auto-adds to organization if needed)
Email Configuration
For password resets and invites. Email templates are located in views/emails/:
password_reset.ecr- Password reset emailorganization_invite.ecr- Organization invitation emailgroup_invite.ecr- Group invitation email
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
SMTP_FROM_EMAIL=noreply@yourdomain.com
SMTP_FROM_NAME=Spider Gazelle
APP_BASE_URL=http://localhost:3000
Gmail Setup: Enable 2FA and generate an App Password at https://myaccount.google.com/apppasswords
Environment Variables
Required:
PG_DATABASE_URL- PostgreSQL connection string
Optional:
SG_ENV- Environment (development/production)SG_SERVER_HOST- Server host (default: 127.0.0.1)SG_SERVER_PORT- Server port (default: 3000)COOKIE_SESSION_SECRET- Session encryption keyGOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRETMICROSOFT_CLIENT_ID/MICROSOFT_CLIENT_SECRETMICROSOFT_TENANT_ID- For single-tenant Microsoft apps
Email (for password reset and invites):
SMTP_HOST- SMTP server hostnameSMTP_PORT- SMTP server portSMTP_USERNAME- SMTP authentication usernameSMTP_PASSWORD- SMTP authentication passwordSMTP_FROM_EMAIL- From email addressSMTP_FROM_NAME- From display nameSMTP_TLS- TLS mode:starttls(default),smtps, ornoneAPP_BASE_URL- Base URL for email links
Other:
PUBLIC_WWW_PATH- Static files directory (default:./www)
Testing
crystal spec
# or
./test
License
Do What the Fuck You Want To Public License
multi-tenancy-template
- 0
- 0
- 0
- 0
- 7
- 2 days ago
- April 4, 2025
Do What The F*ck You Want To Public License
Wed, 14 Jan 2026 13:07:13 GMT