Skip to main content

Core Tables

These tables form the foundation of Artbase Studio's multi-tenant architecture.

profiles

User profile data, linked to Supabase Auth.

CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
email TEXT NOT NULL,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
ColumnTypeDescription
idUUIDUser ID from auth.users
emailTEXTUser's email address
full_nameTEXTDisplay name
avatar_urlTEXTProfile picture URL
created_atTIMESTAMPTZAccount creation time
updated_atTIMESTAMPTZLast profile update

Trigger: Auto-created when user signs up via handle_new_user() function.


organizations

Creator stores/businesses. Each organization is an isolated tenant.

CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT,
logo_url TEXT,
banner_url TEXT,
plan_id UUID REFERENCES plans(id),
is_founding_artist BOOLEAN DEFAULT FALSE,
founding_artist_start_date TIMESTAMPTZ,
stripe_customer_id TEXT,
stripe_subscription_id TEXT,
settings JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_organizations_slug ON organizations(slug);
CREATE INDEX idx_organizations_plan_id ON organizations(plan_id);
ColumnTypeDescription
idUUIDPrimary key
nameTEXTStore display name
slugTEXTURL-friendly identifier
descriptionTEXTStore description
logo_urlTEXTStore logo
banner_urlTEXTStore banner image
plan_idUUIDFK to subscription plan
is_founding_artistBOOLEANFounding artist status
founding_artist_start_dateTIMESTAMPTZWhen founding status began
stripe_customer_idTEXTStripe customer reference
stripe_subscription_idTEXTStripe subscription reference
settingsJSONBCustom organization settings
created_atTIMESTAMPTZCreation timestamp
updated_atTIMESTAMPTZLast update

Settings JSONB Structure

{
"currency": "USD",
"timezone": "America/New_York",
"notifications": {
"new_order": true,
"low_stock": true
}
}

organization_members

Junction table linking users to organizations with roles.

CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(org_id, user_id)
);

CREATE INDEX idx_org_members_user_id ON organization_members(user_id);
CREATE INDEX idx_org_members_org_id ON organization_members(org_id);
ColumnTypeDescription
idUUIDPrimary key
org_idUUIDFK to organization
user_idUUIDFK to auth.users
roleTEXTPermission level
created_atTIMESTAMPTZWhen member was added

Roles

RoleDescription
ownerFull access, can delete organization
adminFull access except billing
memberRead/write products, orders
viewerRead-only access

plans

Subscription tiers with feature definitions.

CREATE TABLE plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
description TEXT,
price_monthly NUMERIC(10,2) NOT NULL,
price_yearly NUMERIC(10,2),
features JSONB NOT NULL DEFAULT '{}'::jsonb,
is_active BOOLEAN DEFAULT TRUE,
sort_order INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
ColumnTypeDescription
idUUIDPrimary key
nameTEXTPlan display name
slugTEXTURL identifier
descriptionTEXTPlan description
price_monthlyNUMERICMonthly price
price_yearlyNUMERICAnnual price (discounted)
featuresJSONBFeature flags and limits
is_activeBOOLEANAvailable for signup
sort_orderINTEGERDisplay order
created_atTIMESTAMPTZCreation timestamp

Features JSONB Structure

{
"max_products": 100,
"max_variants": 10,
"commission_rate": 0.05,
"custom_domain": false,
"analytics_retention_days": 90,
"email_subscribers_limit": 1000,
"channels": ["etsy", "gumroad"],
"priority_support": false
}

Default Plans

PlanPriceCommissionProducts
Free$010%10
Starter$15/mo5%50
Pro$39/mo3%Unlimited
EnterpriseCustom0%Unlimited

organization_settings

Extended settings for per-organization customization.

CREATE TABLE organization_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email_from_name TEXT,
email_from_address TEXT,
email_reply_to TEXT,
shipping_from_address JSONB,
tax_settings JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(org_id)
);

CREATE INDEX idx_org_settings_org_id ON organization_settings(org_id);
ColumnTypeDescription
idUUIDPrimary key
org_idUUIDFK to organization
email_from_nameTEXTSender name for emails
email_from_addressTEXTSender email address
email_reply_toTEXTReply-to address
shipping_from_addressJSONBReturn address
tax_settingsJSONBTax configuration

RLS Policies for Core Tables

-- Profiles: Users can view/edit own profile
CREATE POLICY "Users view own profile"
ON profiles FOR SELECT
USING (id = auth.uid());

CREATE POLICY "Users update own profile"
ON profiles FOR UPDATE
USING (id = auth.uid());

-- Organizations: View through membership
CREATE POLICY "Members view organization"
ON organizations FOR SELECT
USING (id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Organization members: View within same org
CREATE POLICY "Members view org members"
ON organization_members FOR SELECT
USING (org_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid()
));

-- Plans: Public read access
CREATE POLICY "Anyone can view plans"
ON plans FOR SELECT
USING (is_active = true);

Common Queries

Get User's Organizations

SELECT o.*
FROM organizations o
JOIN organization_members om ON om.org_id = o.id
WHERE om.user_id = auth.uid()
ORDER BY o.created_at;

Get Organization with Plan

SELECT
o.*,
p.name as plan_name,
p.features
FROM organizations o
LEFT JOIN plans p ON p.id = o.plan_id
WHERE o.id = $1;

Check User Role in Organization

SELECT role
FROM organization_members
WHERE org_id = $1 AND user_id = auth.uid();