Skip to content
Closed
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
11 changes: 11 additions & 0 deletions apps/playground/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Vercel KV Configuration
# These environment variables are automatically set when you create a KV database in Vercel
# For local development, run: vercel env pull .env.local

# KV_URL=your_kv_url_here
# KV_REST_API_URL=your_kv_rest_api_url_here
# KV_REST_API_TOKEN=your_kv_rest_api_token_here
# KV_REST_API_READ_ONLY_TOKEN=your_kv_rest_api_read_only_token_here

# Note: The application will automatically fall back to in-memory storage
# if these variables are not set, so local development works without KV.
19 changes: 19 additions & 0 deletions apps/playground/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,29 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Dependencies
node_modules

# Next.js
/.next/
/out/
.next

# Build outputs
dist
dist-ssr
*.local

# Environment variables
.env*.local
.env.local
.env.development.local
.env.test.local
.env.production.local

# Vercel
.vercel

# Editor directories and files
.vscode/*
!.vscode/extensions.json
Expand All @@ -22,3 +40,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?

124 changes: 124 additions & 0 deletions apps/playground/VERCEL_KV_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Adding Vercel KV Storage

This guide explains how to add Vercel KV for persistent cloud storage in the Object UI Studio.

## Prerequisites

1. A Vercel account
2. The playground deployed to Vercel (or set up locally with Vercel CLI)

## Step 1: Install Vercel KV Package

Add the `@vercel/kv` package to the playground:

```bash
cd apps/playground
pnpm add @vercel/kv
```

Or if using the root:
```bash
pnpm add @vercel/kv --filter @apps/playground
```

## Step 2: Set Up Vercel KV Database

### Option A: Using Vercel Dashboard

1. Go to your Vercel dashboard: https://vercel.com/dashboard
2. Select your project
3. Go to the "Storage" tab
4. Click "Create Database" → "KV"
5. Name your database (e.g., `objectui-designs`)
6. Click "Create"
7. Copy the environment variables provided

### Option B: Using Vercel CLI

```bash
vercel env pull .env.local
```

## Step 3: Configure Environment Variables

Create a `.env.local` file in `apps/playground/`:

```env
# Vercel KV
KV_URL="your_kv_url_here"
KV_REST_API_URL="your_kv_rest_api_url_here"
KV_REST_API_TOKEN="your_kv_rest_api_token_here"
KV_REST_API_READ_ONLY_TOKEN="your_kv_rest_api_read_only_token_here"
```

These will be automatically set in Vercel when you create the KV database.

## Step 4: Update serverStorage.ts

The storage module has been updated to support both in-memory (fallback) and Vercel KV storage.

Key changes:
- Detects if KV is available via environment variables
- Falls back to in-memory storage if KV is not configured
- Uses Redis-like commands for KV operations
- Stores designs with prefix `design:` and shared designs with prefix `shared:`

## Step 5: Update .gitignore

Add to your `.gitignore`:

```
.env*.local
.vercel
```

## Step 6: Deploy to Vercel

```bash
vercel deploy
```

The KV environment variables will be automatically available in production.

## Usage

No code changes needed in your API routes! The storage module automatically:
1. Uses Vercel KV if available (in production/Vercel environment)
2. Falls back to in-memory storage for local development

## Testing Locally with KV

To test KV locally:

1. Install Vercel CLI: `npm i -g vercel`
2. Link your project: `vercel link`
3. Pull environment variables: `vercel env pull .env.local`
4. Run dev server: `pnpm dev`

## Data Structure in KV

```
design:{id} → JSON string of Design object
shared:{shareId} → JSON string of Design object
designs:all → Set of all design IDs
```

## Monitoring

View your KV database in Vercel Dashboard → Storage → Your KV Database

## Migration from localStorage

The current implementation keeps localStorage on the client side as a cache/fallback. To sync with KV:

1. Client components will make API calls to server routes
2. Server routes use KV for persistence
3. localStorage serves as client-side cache for offline support

## Cost

Vercel KV free tier includes:
- 256 MB storage
- 3000 requests per day

Perfect for prototyping and small projects!
88 changes: 88 additions & 0 deletions apps/playground/app/api/designs/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server';
import { serverStorage } from '@/lib/serverStorage';

/**
* GET /api/designs/:id - Get a single design
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const design = await serverStorage.getDesign(id);

if (!design) {
return NextResponse.json(
{ error: 'Design not found' },
{ status: 404 }
);
}

return NextResponse.json(design);
} catch (error) {
console.error('Error fetching design:', error);
return NextResponse.json(
{ error: 'Failed to fetch design' },
{ status: 500 }
);
}
}

/**
* PUT /api/designs/:id - Update a design
*/
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const body = await request.json();

const updatedDesign = await serverStorage.updateDesign(id, body);

if (!updatedDesign) {
return NextResponse.json(
{ error: 'Design not found' },
{ status: 404 }
);
}

return NextResponse.json(updatedDesign);
} catch (error) {
console.error('Error updating design:', error);
return NextResponse.json(
{ error: 'Failed to update design' },
{ status: 500 }
);
}
}

/**
* DELETE /api/designs/:id - Delete a design
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const deleted = await serverStorage.deleteDesign(id);

if (!deleted) {
return NextResponse.json(
{ error: 'Design not found' },
{ status: 404 }
);
}

return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting design:', error);
return NextResponse.json(
{ error: 'Failed to delete design' },
{ status: 500 }
);
}
}
40 changes: 40 additions & 0 deletions apps/playground/app/api/designs/[id]/share/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server';
import { serverStorage } from '@/lib/serverStorage';

/**
* POST /api/designs/:id/share - Generate share link for a design
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const design = await serverStorage.getDesign(id);

if (!design) {
return NextResponse.json(
{ error: 'Design not found' },
{ status: 404 }
);
}

// Generate a unique share ID
const shareId = crypto.randomUUID().replace(/-/g, '').substring(0, 12);

// Store the shared design
await serverStorage.shareDesign(id, shareId);

// Generate the share URL
const baseUrl = request.nextUrl.origin;
const shareUrl = `${baseUrl}/studio/shared/${shareId}`;

return NextResponse.json({ shareUrl, shareId });
} catch (error) {
console.error('Error sharing design:', error);
return NextResponse.json(
{ error: 'Failed to share design' },
{ status: 500 }
);
}
}
59 changes: 59 additions & 0 deletions apps/playground/app/api/designs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server';
import type { Design } from '@/lib/designStorage';
import { serverStorage } from '@/lib/serverStorage';

/**
* GET /api/designs - List all designs
*/
export async function GET() {
try {
const allDesigns = await serverStorage.getAllDesigns();
return NextResponse.json(allDesigns);
} catch (error) {
console.error('Error fetching designs:', error);
return NextResponse.json(
{ error: 'Failed to fetch designs' },
{ status: 500 }
);
}
}

/**
* POST /api/designs - Create new design
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, description, schema, tags } = body;

if (!name || !schema) {
return NextResponse.json(
{ error: 'Name and schema are required' },
{ status: 400 }
);
}

const id = `design_${crypto.randomUUID()}`;
const now = new Date().toISOString();

const newDesign: Design = {
id,
name,
description,
schema,
tags: tags || [],
createdAt: now,
updatedAt: now,
};

await serverStorage.createDesign(newDesign);

return NextResponse.json(newDesign, { status: 201 });
} catch (error) {
console.error('Error creating design:', error);
return NextResponse.json(
{ error: 'Failed to create design' },
{ status: 500 }
);
}
}
30 changes: 30 additions & 0 deletions apps/playground/app/api/designs/shared/[shareId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextRequest, NextResponse } from 'next/server';
import { serverStorage } from '@/lib/serverStorage';

/**
* GET /api/designs/shared/:shareId - Get a shared design
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ shareId: string }> }
) {
try {
const { shareId } = await params;
const design = await serverStorage.getSharedDesign(shareId);

if (!design) {
return NextResponse.json(
{ error: 'Shared design not found' },
{ status: 404 }
);
}

return NextResponse.json(design);
} catch (error) {
console.error('Error fetching shared design:', error);
return NextResponse.json(
{ error: 'Failed to fetch shared design' },
{ status: 500 }
);
}
}
File renamed without changes.
Loading