Skip to content

Integrate Node 2 Supabase for app preview data storage #13

@ebowwa

Description

@ebowwa

Summary

Connect app-preview-generator to Node 2 Supabase for data storage.

Quick Start (2 minutes)

1. Setup Doppler

# Install (if needed)
brew install dopplerhq/cli/doppler  # macOS
# or
curl -Ls https://cli.doppler.com/install.sh | sudo sh  # Linux

# Login & setup
doppler login
doppler setup --project hetzner-infrastructure --config prd

2. Add to your code

// Install
npm install @supabase/supabase-js

// Use in your app
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
)

3. Run your app

doppler run -- npm start
# or for development
doppler run -- npm run dev

Implementation Strategy

Based on analysis of the app-preview-generator codebase, here's how we'll integrate Supabase:

Key Data Points to Store

From the Screen interface (v002/types/preview-generator.ts), we'll persist:

  • Text content: title, subtitle, description
  • Visual settings: colors, overlay styles, positions, opacity
  • Screenshot data: URLs, positions, transformations
  • Image assets: Additional overlays with positioning
  • Device configurations: Type, orientation, export settings

Database Schema

-- Projects table
CREATE TABLE projects (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  device_type TEXT DEFAULT 'iphone-69',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW(),
  user_id UUID REFERENCES auth.users(id),
  metadata JSONB
);

-- Screens table (multiple per project)
CREATE TABLE screens (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
  position INT NOT NULL,
  
  -- Text content
  title TEXT,
  subtitle TEXT,
  description TEXT,
  
  -- Visual style settings
  overlay_style TEXT DEFAULT 'gradient',
  text_position TEXT DEFAULT 'bottom',
  device_position TEXT DEFAULT 'center',
  bg_style TEXT DEFAULT 'gradient',
  layout_style TEXT DEFAULT 'standard',
  layer_order TEXT DEFAULT 'front',
  
  -- Colors
  primary_color TEXT DEFAULT '#4F46E5',
  secondary_color TEXT DEFAULT '#7C3AED',
  bg_color TEXT DEFAULT '#F3F4F6',
  
  -- Positions stored as JSONB
  position JSONB DEFAULT '{"x":0,"y":0,"scale":100,"rotation":0}',
  text_overlay_position JSONB DEFAULT '{"x":0,"y":0}',
  opacity JSONB DEFAULT '{"screenshot":100,"overlay":90}',
  
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Screenshots table (multiple per screen)
CREATE TABLE screenshots (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  screen_id UUID REFERENCES screens(id) ON DELETE CASCADE,
  url TEXT NOT NULL,
  position JSONB DEFAULT '{"x":0,"y":0,"scale":100,"rotation":0}',
  opacity INT DEFAULT 100,
  z_index INT DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Image assets table (logos, badges, etc.)
CREATE TABLE image_assets (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  screen_id UUID REFERENCES screens(id) ON DELETE CASCADE,
  url TEXT NOT NULL,
  position JSONB DEFAULT '{"x":0,"y":0}',
  size JSONB DEFAULT '{"width":100,"height":100}',
  rotation INT DEFAULT 0,
  opacity INT DEFAULT 100,
  z_index INT DEFAULT 10,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Integration Points in Current Code

  1. Save Project (page.tsx:552-556)

    • Currently saves to JSON file
    • Will save to Supabase projects and related tables
  2. Load Project (page.tsx:558-578)

    • Currently loads from JSON file
    • Will fetch from Supabase using project ID
  3. Image Upload (page.tsx:403-429)

    • Currently stores as data URLs
    • Will upload to Supabase Storage, store URLs in DB
  4. Real-time Collaboration

    • Add Supabase real-time subscriptions
    • Multiple users can work on same project
  5. Export Functions (page.tsx:520-550)

    • Store generated preview URLs
    • Track export history

Code Integration Example

// Example integration in page.tsx
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_KEY!
)

// Save project to Supabase
const saveProjectToSupabase = async () => {
  const { data: project, error } = await supabase
    .from('projects')
    .upsert({
      name: `Preview-${Date.now()}`,
      device_type: deviceType,
      metadata: { version: '2.0' }
    })
    .select()
    .single()
  
  if (!error && project) {
    // Save screens
    for (const [index, screen] of screens.entries()) {
      const { data: savedScreen } = await supabase
        .from('screens')
        .insert({
          project_id: project.id,
          position: index,
          title: screen.title,
          subtitle: screen.subtitle,
          description: screen.description,
          overlay_style: screen.overlayStyle,
          text_position: screen.textPosition,
          device_position: screen.devicePosition,
          bg_style: screen.bgStyle,
          layout_style: screen.layoutStyle,
          layer_order: screen.layerOrder,
          primary_color: screen.primaryColor,
          secondary_color: screen.secondaryColor,
          bg_color: screen.bgColor,
          position: screen.position,
          text_overlay_position: screen.textOverlayPosition,
          opacity: screen.opacity
        })
        .select()
        .single()
      
      // Save screenshots
      if (screen.screenshots?.length > 0) {
        await supabase.from('screenshots').insert(
          screen.screenshots.map(s => ({
            screen_id: savedScreen.id,
            url: s.url, // Will be storage URL after upload
            position: s.position,
            opacity: s.opacity,
            z_index: s.zIndex
          }))
        )
      }
      
      // Save image assets
      if (screen.imageAssets?.length > 0) {
        await supabase.from('image_assets').insert(
          screen.imageAssets.map(a => ({
            screen_id: savedScreen.id,
            url: a.url,
            position: a.position,
            size: a.size,
            rotation: a.rotation,
            opacity: a.opacity,
            z_index: a.zIndex
          }))
        )
      }
    }
  }
}

// Load project from Supabase
const loadProjectFromSupabase = async (projectId: string) => {
  const { data: project } = await supabase
    .from('projects')
    .select(`
      *,
      screens (
        *,
        screenshots (*),
        image_assets (*)
      )
    `)
    .eq('id', projectId)
    .single()
  
  if (project) {
    // Map to local state format
    const mappedScreens = project.screens.map(s => ({
      id: s.id,
      title: s.title,
      subtitle: s.subtitle,
      description: s.description,
      overlayStyle: s.overlay_style,
      textPosition: s.text_position,
      devicePosition: s.device_position,
      bgStyle: s.bg_style,
      layoutStyle: s.layout_style,
      layerOrder: s.layer_order,
      primaryColor: s.primary_color,
      secondaryColor: s.secondary_color,
      bgColor: s.bg_color,
      position: s.position,
      textOverlayPosition: s.text_overlay_position,
      opacity: s.opacity,
      screenshots: s.screenshots,
      imageAssets: s.image_assets,
      screenshot: s.screenshots?.[0]?.url || null // Legacy support
    }))
    setScreens(mappedScreens)
    setDeviceType(project.device_type)
  }
}

// Upload image to Supabase Storage
const uploadToStorage = async (file: File, bucket = 'screenshots') => {
  const fileExt = file.name.split('.').pop()
  const fileName = `${Math.random()}.${fileExt}`
  const filePath = `${userId}/${fileName}`
  
  const { data, error } = await supabase.storage
    .from(bucket)
    .upload(filePath, file)
  
  if (!error) {
    const { data: { publicUrl } } = supabase.storage
      .from(bucket)
      .getPublicUrl(filePath)
    
    return publicUrl
  }
  return null
}

// Real-time subscription for collaboration
supabase
  .channel('project-changes')
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'screens',
    filter: `project_id=eq.${projectId}`
  }, (payload) => {
    // Update local state with changes
    console.log('Screen updated:', payload)
    // Refresh screens from database
  })
  .subscribe()

Benefits

  1. Persistent Storage: Projects saved in cloud, accessible anywhere
  2. Collaboration: Multiple users can work on same project
  3. Asset Management: Centralized storage for all images/screenshots
  4. Version History: Track changes over time
  5. Export Tracking: Store generated previews with metadata
  6. Search & Filter: Find projects by text content, colors, device type

Environment Variables (auto-injected by Doppler)

SUPABASE_URL          # API endpoint
SUPABASE_KEY          # Access key (anon or service based on needs)
DATABASE_URL          # Direct PostgreSQL connection

Implementation Steps

  1. Get Doppler access from team admin
  2. Run setup command above
  3. Create database schema in Supabase
  4. Add Supabase client to Next.js app
  5. Implement save/load functions
  6. Add real-time subscriptions
  7. Migrate image uploads to Supabase Storage

Notes

  • Node 2 Host: 91.98.132.37
  • All credentials managed by Doppler - never hardcode
  • Use doppler run to inject credentials automatically

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions