Package Exports
- @dasheck0/supabase-saas-domain
- @dasheck0/supabase-saas-domain/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@dasheck0/supabase-saas-domain) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
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.