Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions infrastructure/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ REDIS_URL=redis://localhost:6379
# For production with authentication:
# REDIS_URL=redis://:password@host:6379

# ===================
# SUPABASE (Database & Auth)
# ===================
# Get these from https://supabase.com/dashboard/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# JWT secret for custom token generation (from Supabase dashboard > Settings > API)
SUPABASE_JWT_SECRET=your-supabase-jwt-secret

# ===================
# AUTHENTICATION (NextAuth.js)
# ===================
Expand All @@ -29,6 +39,17 @@ NEXTAUTH_SECRET=your-nextauth-secret-minimum-32-characters-long
# JWT secret for API authentication (can be same as NEXTAUTH_SECRET or different)
JWT_SECRET=your-jwt-secret-minimum-32-characters-long

# ===================
# OAUTH PROVIDERS (Optional)
# ===================
# Google OAuth - https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxx

# GitHub OAuth - https://github.com/settings/developers
GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx

# ===================
# STRIPE (Payments)
# ===================
Expand Down
262 changes: 262 additions & 0 deletions supabase/migrations/001_initial_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
-- LARUN Database Schema for Supabase
-- Migration: 001_initial_schema
-- Description: Initial database setup with users, analyses, and subscriptions

-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- ===========================================
-- USERS TABLE
-- ===========================================
CREATE TABLE IF NOT EXISTS public.users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT UNIQUE NOT NULL,
name TEXT,
image TEXT,
email_verified TIMESTAMPTZ,
subscription_tier TEXT DEFAULT 'free' CHECK (subscription_tier IN ('free', 'hobbyist', 'professional')),
stripe_customer_id TEXT UNIQUE,
analyses_this_month INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for email lookups
CREATE INDEX IF NOT EXISTS idx_users_email ON public.users(email);
CREATE INDEX IF NOT EXISTS idx_users_stripe_customer ON public.users(stripe_customer_id);

-- ===========================================
-- ACCOUNTS TABLE (for NextAuth)
-- ===========================================
CREATE TABLE IF NOT EXISTS public.accounts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
type TEXT NOT NULL,
provider TEXT NOT NULL,
provider_account_id TEXT NOT NULL,
refresh_token TEXT,
access_token TEXT,
expires_at BIGINT,
token_type TEXT,
scope TEXT,
id_token TEXT,
session_state TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(provider, provider_account_id)
);

CREATE INDEX IF NOT EXISTS idx_accounts_user_id ON public.accounts(user_id);

-- ===========================================
-- SESSIONS TABLE (for NextAuth)
-- ===========================================
CREATE TABLE IF NOT EXISTS public.sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
session_token TEXT UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
expires TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON public.sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_token ON public.sessions(session_token);

-- ===========================================
-- VERIFICATION TOKENS TABLE (for NextAuth)
-- ===========================================
CREATE TABLE IF NOT EXISTS public.verification_tokens (
identifier TEXT NOT NULL,
token TEXT UNIQUE NOT NULL,
expires TIMESTAMPTZ NOT NULL,
PRIMARY KEY (identifier, token)
);

-- ===========================================
-- ANALYSES TABLE
-- ===========================================
CREATE TABLE IF NOT EXISTS public.analyses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
tic_id TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
result JSONB,
error_message TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ
);

CREATE INDEX IF NOT EXISTS idx_analyses_user_id ON public.analyses(user_id);
CREATE INDEX IF NOT EXISTS idx_analyses_status ON public.analyses(status);
CREATE INDEX IF NOT EXISTS idx_analyses_tic_id ON public.analyses(tic_id);
CREATE INDEX IF NOT EXISTS idx_analyses_created_at ON public.analyses(created_at DESC);

-- ===========================================
-- SUBSCRIPTIONS TABLE
-- ===========================================
CREATE TABLE IF NOT EXISTS public.subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
stripe_subscription_id TEXT UNIQUE NOT NULL,
stripe_price_id TEXT NOT NULL,
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'canceled', 'past_due', 'trialing', 'incomplete')),
current_period_start TIMESTAMPTZ NOT NULL,
current_period_end TIMESTAMPTZ NOT NULL,
cancel_at_period_end BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON public.subscriptions(user_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_id ON public.subscriptions(stripe_subscription_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON public.subscriptions(status);

-- ===========================================
-- USAGE TRACKING TABLE
-- ===========================================
CREATE TABLE IF NOT EXISTS public.usage_records (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
analysis_id UUID REFERENCES public.analyses(id) ON DELETE SET NULL,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
count INTEGER DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_usage_user_period ON public.usage_records(user_id, period_start, period_end);

-- ===========================================
-- ROW LEVEL SECURITY (RLS)
-- ===========================================

-- Enable RLS on all tables
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.analyses ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.subscriptions ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.usage_records ENABLE ROW LEVEL SECURITY;

-- Users can only see their own data
CREATE POLICY "Users can view own profile"
ON public.users FOR SELECT
USING (auth.uid() = id);

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

-- Analyses policies
CREATE POLICY "Users can view own analyses"
ON public.analyses FOR SELECT
USING (auth.uid() = user_id);

CREATE POLICY "Users can create own analyses"
ON public.analyses FOR INSERT
WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can delete own analyses"
ON public.analyses FOR DELETE
USING (auth.uid() = user_id);

-- Subscriptions policies
CREATE POLICY "Users can view own subscriptions"
ON public.subscriptions FOR SELECT
USING (auth.uid() = user_id);

-- Usage records policies
CREATE POLICY "Users can view own usage"
ON public.usage_records FOR SELECT
USING (auth.uid() = user_id);

-- ===========================================
-- FUNCTIONS
-- ===========================================

-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';

-- Apply updated_at trigger to relevant tables
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_accounts_updated_at
BEFORE UPDATE ON public.accounts
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_sessions_updated_at
BEFORE UPDATE ON public.sessions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_subscriptions_updated_at
BEFORE UPDATE ON public.subscriptions
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- Function to reset monthly usage counters
CREATE OR REPLACE FUNCTION reset_monthly_usage()
RETURNS void AS $$
BEGIN
UPDATE public.users
SET analyses_this_month = 0
WHERE analyses_this_month > 0;
END;
$$ language 'plpgsql';

-- Function to increment user analysis count
CREATE OR REPLACE FUNCTION increment_analysis_count(p_user_id UUID)
RETURNS void AS $$
BEGIN
UPDATE public.users
SET analyses_this_month = analyses_this_month + 1
WHERE id = p_user_id;
END;
$$ language 'plpgsql';

-- Function to check if user can analyze (within limits)
CREATE OR REPLACE FUNCTION can_user_analyze(p_user_id UUID)
RETURNS BOOLEAN AS $$
DECLARE
v_tier TEXT;
v_count INTEGER;
v_limit INTEGER;
BEGIN
SELECT subscription_tier, analyses_this_month
INTO v_tier, v_count
FROM public.users
WHERE id = p_user_id;

-- Determine limit based on tier
CASE v_tier
WHEN 'professional' THEN v_limit := -1; -- Unlimited
WHEN 'hobbyist' THEN v_limit := 25;
ELSE v_limit := 3; -- Free tier
END CASE;

-- Return true if unlimited or under limit
RETURN v_limit = -1 OR v_count < v_limit;
END;
$$ language 'plpgsql';

-- ===========================================
-- INITIAL DATA (optional)
-- ===========================================

-- No initial data needed - users will sign up through the app

COMMENT ON TABLE public.users IS 'User profiles and subscription information';
COMMENT ON TABLE public.analyses IS 'Exoplanet transit analyses submitted by users';
COMMENT ON TABLE public.subscriptions IS 'Stripe subscription records';
COMMENT ON TABLE public.usage_records IS 'Monthly usage tracking for billing';
Loading
Loading