← back to Small Business Builder
migrations/001_init.sql
115 lines
-- small-business-builder — initial schema
-- DB: small_business_directory (standalone, NOT dw_unified)
BEGIN;
CREATE TABLE IF NOT EXISTS businesses (
id BIGSERIAL PRIMARY KEY,
slug TEXT UNIQUE,
name TEXT NOT NULL,
category TEXT, -- salon, barbershop, nail_salon, generic
address TEXT,
city TEXT,
state TEXT,
zip TEXT,
latitude NUMERIC,
longitude NUMERIC,
phone TEXT,
email TEXT,
website TEXT,
years_in_business INT,
owner_name TEXT,
hero_image_url TEXT,
color_primary TEXT,
color_accent TEXT,
about_text TEXT,
hours_json JSONB,
tier TEXT NOT NULL DEFAULT 'free' CHECK (tier IN ('free','plus','premium')),
ranking_score NUMERIC,
source_data_json JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_businesses_slug ON businesses(slug);
CREATE INDEX IF NOT EXISTS idx_businesses_category ON businesses(category);
CREATE INDEX IF NOT EXISTS idx_businesses_city_state ON businesses(city, state);
CREATE INDEX IF NOT EXISTS idx_businesses_tier ON businesses(tier);
CREATE TABLE IF NOT EXISTS business_socials (
id BIGSERIAL PRIMARY KEY,
business_id BIGINT REFERENCES businesses(id) ON DELETE CASCADE,
platform TEXT, -- instagram, tiktok, twitter, linkedin, facebook, youtube
handle TEXT,
url TEXT,
follower_count INT,
last_post_at TIMESTAMPTZ,
raw_json JSONB,
scraped_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_socials_business ON business_socials(business_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_socials_business_platform
ON business_socials(business_id, platform);
CREATE TABLE IF NOT EXISTS business_inputs (
-- free-form structured input from owner: "anything clients should know" + arbitrary key-value
id BIGSERIAL PRIMARY KEY,
business_id BIGINT REFERENCES businesses(id) ON DELETE CASCADE,
field_key TEXT NOT NULL,
field_value TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_inputs_business ON business_inputs(business_id);
CREATE TABLE IF NOT EXISTS business_mockups (
id BIGSERIAL PRIMARY KEY,
business_id BIGINT REFERENCES businesses(id) ON DELETE CASCADE,
template_id INT, -- 1..5
preview_url TEXT,
full_html TEXT,
generated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_mockups_business ON business_mockups(business_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_mockups_business_template
ON business_mockups(business_id, template_id);
CREATE TABLE IF NOT EXISTS tiers (
id SERIAL PRIMARY KEY,
code TEXT UNIQUE,
name TEXT,
monthly_usd NUMERIC,
features_json JSONB
);
INSERT INTO tiers (code, name, monthly_usd, features_json) VALUES
('free', 'Free Listing', 0,
'["directory listing", "name + address + hours + phone", "1 hero image"]'),
('plus', 'Plus', 19,
'["full website (5 templates)", "custom domain via Cloudflare", "lead form", "Stripe payments", "no builder branding"]'),
('premium', 'Premium', 79,
'["everything in Plus", "booking/calendar integration", "SEO autosubmission", "social-post auto-scheduler", "monthly performance email"]')
ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS leads (
id BIGSERIAL PRIMARY KEY,
business_id BIGINT REFERENCES businesses(id),
client_email TEXT,
client_phone TEXT,
message TEXT,
status TEXT DEFAULT 'new',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_leads_business ON leads(business_id);
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads(status);
-- updated_at trigger
CREATE OR REPLACE FUNCTION smb_set_updated_at() RETURNS TRIGGER AS $$
BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_businesses_updated_at ON businesses;
CREATE TRIGGER trg_businesses_updated_at
BEFORE UPDATE ON businesses
FOR EACH ROW EXECUTE FUNCTION smb_set_updated_at();
COMMIT;