@dasheck0/supabase-saas-domain
v1.0.4
Published
Opinionated TypeScript framework for building multi-tenant SaaS applications with Supabase. Includes database setup, React Query integration, Zustand state management, and complete tenant isolation.
Downloads
141
Maintainers
Readme
@dasheck0/supabase-saas-domain
An opinionated TypeScript framework for building multi-tenant SaaS applications with Supabase. This package provides a complete domain layer with authentication, tenant management, profile handling, storage, and row-level security (RLS) patterns.
🌟 Philosophy
This is an opinionated framework that makes specific architectural decisions for you:
- Multi-tenant by design: Every data operation is tenant-scoped
- RLS-first security: Database-level security with proper tenant isolation
- React Query integration: Optimistic updates, caching, and synchronization
- Zustand state management: Minimal, persistent client state
- TypeScript-native: Full type safety across the stack
🚀 Quick Start
Installation
Requirements:
- Node.js >=20.0.0
- npm >=10.0.0
npm install @dasheck0/supabase-saas-domain @tanstack/react-query zustandDatabase Setup
The framework requires a specific database schema with multi-tenant support, RLS policies, and storage configuration. You have several options to set this up:
Option 1: CLI Tool (Recommended)
# Install the package
npm install @dasheck0/supabase-saas-domain
# Run the setup with your Supabase credentials
npx supabase-saas-setup run --url YOUR_SUPABASE_URL --key YOUR_SERVICE_ROLE_KEY --verbose
# Or use environment variables
export SUPABASE_URL=your_supabase_url
export SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
npx supabase-saas-setup quickOption 2: Programmatic Setup
const { setupSaasDatabase } = require('@dasheck0/supabase-saas-domain');
const { createClient } = require('@supabase/supabase-js');
const supabaseClient = createClient(
'your-supabase-url',
'your-service-role-key',
{ auth: { autoRefreshToken: false, persistSession: false } }
);
const result = await setupSaasDatabase({
supabaseClient,
verbose: true
});
if (result.success) {
console.log('✅ Database setup complete!');
} else {
console.error('❌ Setup failed:', result.errors);
}Option 3: Manual Migration
- Copy all migration files from
/supabase/migrations/in the package - Apply them to your Supabase project using the Supabase CLI or Dashboard
- Create a storage bucket named 'uploads' with public access
What Gets Created
- Tables:
profiles,tenants,tenant_membershipswith proper relationships - RLS Policies: Tenant-scoped security for all data operations
- Storage Bucket: 'uploads' bucket with RLS policies for file management
- Database Functions: Utility functions for tenant management and SQL execution
- Triggers: Automatic profile and tenant creation on user signup
- Migration Tracking:
schema_migrationstable to prevent duplicate setups
Note: The setup tool uses embedded SQL migrations (not file-based) making the package completely self-contained. This means you don't need to manually copy migration files - everything is bundled with the npm package.
Basic Setup
import { SaasProvider } from '@dasheck0/supabase-saas-domain';
import { createClient } from '@supabase/supabase-js';
const supabaseClient = createClient(
'your-supabase-url',
'your-supabase-anon-key'
);
function App() {
return (
<SaasProvider supabaseClient={supabaseClient}>
<YourApp />
</SaasProvider>
);
}Authentication Sync
The framework requires you to sync authentication state:
import { useSaasUtils } from '@dasheck0/supabase-saas-domain';
function AuthSync({ user }: { user: any }) {
const { setAuthentication, getUserId } = useSaasUtils();
useEffect(() => {
const currentUserId = getUserId();
const newUserId = user?.id || null;
if (currentUserId !== newUserId) {
if (newUserId) {
setAuthentication(newUserId, null);
} else {
setAuthentication(null, null);
}
}
}, [user?.id, setAuthentication, getUserId]);
return null;
}🏗 Core Concepts
1. Multi-Tenant Architecture
The framework enforces a tenant-first approach:
import { useSaasTenantHooks, useTenantSwitcher } from '@dasheck0/supabase-saas-domain';
function TenantManager() {
const { useTenants, useCreateTenant } = useSaasTenantHooks();
const { currentTenant, switchTenant } = useTenantSwitcher();
const { data: tenants } = useTenants();
const createTenant = useCreateTenant();
const handleCreateTenant = async () => {
const newTenant = await createTenant.mutateAsync({
name: 'My Company',
description: 'Company workspace'
});
switchTenant(newTenant);
};
return (
<div>
<h2>Current: {currentTenant?.name}</h2>
{tenants?.map(tenant => (
<button key={tenant.id} onClick={() => switchTenant(tenant)}>
{tenant.name}
</button>
))}
</div>
);
}2. Profile Management
User profiles with tenant-scoped visibility:
import { useSaasProfileHooks } from '@dasheck0/supabase-saas-domain';
function ProfileManager() {
const { useProfiles, useCreateProfile, useUpdateProfile } = useSaasProfileHooks();
const { data: profiles } = useProfiles(); // Only profiles visible to current user
const createProfile = useCreateProfile();
const updateProfile = useUpdateProfile();
return (
<div>
{profiles?.map(profile => (
<div key={profile.id}>
{profile.firstName} {profile.lastName}
<img src={profile.imageUrl} alt="Avatar" />
</div>
))}
</div>
);
}3. Storage with Tenant Isolation
Built-in file storage with automatic tenant scoping:
import { useSaasStorageHooks, ImageUpload } from '@dasheck0/supabase-saas-domain';
function AvatarUpload({ userId }: { userId: string }) {
const { useUploadAvatar } = useSaasStorageHooks();
const { currentTenant } = useTenantSwitcher();
const uploadAvatar = useUploadAvatar();
const handleUpload = async (file: File) => {
if (currentTenant) {
const result = await uploadAvatar.mutateAsync({
userId,
tenantId: currentTenant.id,
file
});
console.log('Uploaded:', result.signedUrl);
}
};
return (
<ImageUpload
onImageSelected={handleUpload}
placeholder="Upload avatar"
/>
);
}4. Membership Management
Handle tenant memberships with role-based access:
import { useSaasMembershipHooks } from '@dasheck0/supabase-saas-domain';
function MembershipManager({ tenantId }: { tenantId: string }) {
const { useTenantMemberships, useCreateTenantMembership } = useSaasMembershipHooks();
const { data: memberships } = useTenantMemberships(tenantId, true); // eager load profiles
const createMembership = useCreateTenantMembership();
const inviteUser = async (email: string) => {
await createMembership.mutateAsync({
tenantId,
userId: email, // In real app, resolve email to userId
role: 'member'
});
};
return (
<div>
<h3>Team Members</h3>
{memberships?.map(membership => (
<div key={membership.id}>
{membership.profile?.firstName} - {membership.role}
</div>
))}
</div>
);
}🛠 Core Hooks API
Tenant Hooks
useTenants()- List accessible tenantsuseCreateTenant()- Create new tenantuseUpdateTenant()- Update tenant detailsuseDeleteTenant()- Delete tenantuseTenantSwitcher()- Switch between tenants
Profile Hooks
useProfiles()- List visible profilesuseCreateProfile()- Create user profileuseUpdateProfile()- Update profileuseDeleteProfile()- Delete profile
Membership Hooks
useTenantMemberships(tenantId)- List tenant membersuseUserMemberships(userId)- List user's membershipsuseCreateTenantMembership()- Add member to tenantuseUpdateTenantMembership()- Update member roleuseDeleteTenantMembership()- Remove member
Storage Hooks
useUploadAvatar()- Upload profile picturesuseUploadTenantImage()- Upload tenant logosuseSignedUrl()- Get signed URLs for filesuseDeleteFile()- Delete stored files
Utility Hooks
useAuthState()- Current authentication stateuseSaasUtils()- Framework utilities
🔒 Security Model
Row-Level Security (RLS)
The framework enforces security at the database level:
-- Users can only see profiles of people in their shared tenants
CREATE POLICY "profiles_shared_tenants" ON public.profiles
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.tenant_memberships tm1
JOIN public.tenant_memberships tm2 ON tm1.tenant_id = tm2.tenant_id
WHERE tm1.user_id = auth.uid()
AND tm2.user_id = profiles.user_id
)
);Tenant Isolation
All data operations are automatically scoped to accessible tenants:
- Storage: Files uploaded to
{tenantId}/category/{userId}/filename - Queries: Filtered by tenant membership
- Mutations: Validated against tenant permissions
📁 Database Schema
The framework expects these core tables:
-- Core Tables
profiles (id, user_id, first_name, last_name, image_url, ...)
tenants (id, name, description, image_url, personal, ...)
tenant_memberships (id, tenant_id, user_id, role, ...)
-- Storage Bucket
uploads (tenant-scoped file storage)🎨 UI Components
ImageUpload Component
import { ImageUpload } from '@dasheck0/supabase-saas-domain';
<ImageUpload
currentImageUrl={profile.imageUrl}
onImageSelected={(file) => handleUpload(file)}
onImageCleared={() => setImageUrl('')}
placeholder="Upload profile picture"
/>Built-in Components
ImageUpload- Drag & drop file upload with previewSaasProvider- Framework provider component
⚙️ Configuration
Required Props
interface SaasProviderProps {
supabaseClient: SupabaseClient; // Your Supabase client instance
children: React.ReactNode;
}Environment Setup
- Supabase Project: Set up with authentication enabled
- Database Migrations: Run provided migrations for schema
- RLS Policies: Apply security policies for tenant isolation
- Storage Bucket: Create 'uploads' bucket for file storage
- Authentication: Configure Supabase Auth providers
� Troubleshooting
Setup Issues
"Cannot find module 'commander'" during CLI setup
# Install dependencies if using the CLI directly
npm install -g @dasheck0/supabase-saas-domain"SQL execution failed" during migration
- Ensure you're using the service role key (not anon key)
- Check that your Supabase project has the necessary permissions
- Verify your Supabase URL is correct
"Multiple GoTrueClient instances detected"
- The framework uses
peerDependenciesto avoid this - Ensure you're passing your app's Supabase client to
SaasProvider - Don't create multiple Supabase clients in your app
"userId is null" in hooks
- Make sure you're using the
AuthSynccomponent - Verify that authentication state is being set properly
- Check that your user is actually authenticated
RLS Policy Issues
"new row violates row-level security policy"
- Ensure the user has a profile record (auto-created on signup)
- Verify the user is a member of the tenant they're trying to access
- Check that tenant memberships are properly configured
Storage upload failures
- Confirm the 'uploads' bucket exists and has proper RLS policies
- Verify the user is authenticated and has tenant membership
- Check file size limits and allowed file types
Performance Tips
- Use the
enabledoption in hooks to prevent unnecessary queries - Implement proper loading states for better UX
- Consider pagination for large datasets
- Use optimistic updates for better perceived performance
�🚀 Development
Package Development
npm install
npm run build # Build TypeScript
npm run dev # Watch mode
npm run test # Run testsExample App
cd examples/react-admin
npm install
npm run dev # Start example app📦 Package Structure
src/
├── api/ # Data layer (Supabase queries)
├── components/ # React components
├── hooks/ # React Query hooks
├── models/ # TypeScript models
├── stores/ # Zustand stores
├── types/ # Type definitions
└── index.ts # Main exports
examples/
└── react-admin/ # Full example application
supabase/
└── migrations/ # Database migrations🤝 Contributing
This is an opinionated framework with specific architectural decisions. Contributions should align with:
- Multi-tenant architecture patterns
- RLS-first security model
- React Query + Zustand state management
- TypeScript-first development
📄 License
MIT © dasheck0
Note: This is an opinionated framework that makes architectural decisions for you. It's designed for teams building multi-tenant SaaS applications who want a complete, secure foundation without reinventing tenant isolation, security patterns, and data management.
