npm package: @symulate/sdk
Description: AI-powered frontend-first development toolkit - Build frontends without waiting for backends
Latest version: 1.5.2

# Symulate SDK

AI-powered frontend-first development toolkit. Build your frontend without waiting for a backend.

## Features

- **🤖 AI-Powered Mocks** - Realistic data generated by OpenAI with intelligent caching
- **⚡ Faker.js Mode** - Offline mode for CI/CD pipelines - no AI dependency required
- **🗄️ Stateful Collections** - Full CRUD operations with persistent state
- **🔗 Interconnected Collections** - Database relationships with foreign keys (belongsTo, hasMany, hasOne)
- **🔒 Type-Safe** - Full TypeScript support with automatic type inference
- **📊 Database Schema Sync** - Auto-generate schemas from database tables with `dbTable()`
- **🎯 Smart Caching** - In-memory, localStorage, or file-based caching strategies
- **🔄 Seamless Production Switch** - One config change to switch from mocks to real backend
- **📝 OpenAPI Generation** - Auto-generate API specs for backend developers
- **✅ CI/CD Ready** - Deterministic Faker.js mode with seed support for testing

## Installation

```bash
npm install @symulate/sdk
```

## Quick Start

### 1. Faker Mode (100% Free, No API Key)

Perfect for CI/CD, testing, and basic prototyping.

```typescript
import { defineEndpoint, configureSymulate, m, type Infer } from "@symulate/sdk";

// Configure - no API key needed!
configureSymulate({
  generateMode: "faker",
  environment: "development",
  backendBaseUrl: "https://api.myapp.com",
});

// Define schema
const UserSchema = m.object({
  id: m.uuid(),
  name: m.person.fullName(),
  email: m.email(),
});

// Infer TypeScript type
type User = Infer<typeof UserSchema>;

// Define endpoint
export const getUsers = defineEndpoint<User[]>({
  path: "/api/users",
  method: "GET",
  schema: UserSchema,
  mock: { count: 5 },
});

// Use it
const users = await getUsers(); // Fully typed as User[]
```

### 2. BYOK Mode (Bring Your Own OpenAI Key)

Get AI-powered realistic data with your own OpenAI API key.

```typescript
configureSymulate({
  openaiApiKey: process.env.OPENAI_API_KEY,
  openaiModel: "gpt-4o-mini", // Optional - specify model (default: gpt-4o-mini)
  generateMode: "ai",
  environment: "development",
  backendBaseUrl: "https://api.myapp.com",
});
```

**Supported models:**
- `gpt-4o-mini` (default) - $0.15/$0.60 per 1M tokens
- `gpt-4o` - $2.50/$10.00 per 1M tokens
- `gpt-4-turbo` - $10.00/$30.00 per 1M tokens
- `gpt-4` - $30.00/$60.00 per 1M tokens
- `gpt-3.5-turbo` - $0.50/$1.50 per 1M tokens

## Stateful Collections

Define collections with full CRUD operations and persistent state:

```typescript
import { defineCollection, m, type Infer } from '@symulate/sdk';

const ProductSchema = m.object({
  id: m.uuid(),
  name: m.string(),
  price: m.number({ min: 10, max: 1000 }),
  category: m.string(),
  inStock: m.boolean(),
});

export type Product = Infer<typeof ProductSchema>;

export const products = defineCollection<Product>({
  name: "products",
  basePath: "/api/products",
  schema: ProductSchema,
  seedCount: 20,
  seedInstruction: "Generate realistic e-commerce products",
});

// Use it with full CRUD
const { data } = await products.list({ page: 1, limit: 10 });
const newProduct = await products.create({
  name: "New Item",
  price: 29.99,
  category: "Electronics",
  inStock: true
});
await products.update(newProduct.id, { price: 24.99 });
await products.delete(newProduct.id);
```

### Persistence Modes

Configure where collection data is stored:

```typescript
configureSymulate({
  collections: {
    persistence: {
      mode: "memory", // "memory" (default), "local", or "cloud"
      filePath: ".symulate-data.json" // For Node.js file persistence
    }
  }
});
```

- **`memory`** - In-memory only (data lost on refresh) - fastest
- **`local`** - localStorage (browser) or file (Node.js) - persists across reloads
- **`cloud`** - Server-side via Supabase (requires API key) - multi-tenant with branch isolation

## Interconnected Collections

Define relationships between collections for realistic, interconnected data - just like a real database.

### Basic Relationships

```typescript
import { defineCollection, m, type Infer } from '@symulate/sdk';

// Define Users collection
const UserSchema = m.object({
  id: m.uuid(),
  name: m.person.fullName(),
  email: m.email(),
});

export const users = defineCollection<Infer<typeof UserSchema>>({
  name: "users",
  basePath: "/api/users",
  schema: UserSchema,
  seedCount: 50,
});

// Define Purchases collection with relation to Users
const PurchaseSchema = m.object({
  id: m.uuid(),
  userId: m.uuid(),
  amount: m.number({ min: 10, max: 500 }),
  createdAt: m.date(),
});

// Response schema with joined user data
const PurchaseResponseSchema = m.object({
  id: m.uuid(),
  userId: m.uuid(),
  amount: m.number(),
  createdAt: m.date(),
  userEmail: m.join('user', 'email'),  // Join user's email
  userName: m.join('user', 'name'),    // Join user's name
});

export const purchases = defineCollection({
  name: "purchases",
  basePath: "/api/purchases",
  schema: PurchaseSchema,               // Storage schema
  responseSchema: PurchaseResponseSchema, // Response includes joins
  seedCount: 200,
  relations: {
    user: {
      type: 'belongsTo',
      collection: 'users',
      foreignKey: 'userId',
      references: 'id'
    }
  }
});

// Use it - joins happen automatically
const { data } = await purchases.list();
// Returns: [{ id, userId, amount, createdAt, userEmail, userName }, ...]
```

### Relation Types

**belongsTo** - One-to-one or many-to-one relationship
```typescript
relations: {
  user: {
    type: 'belongsTo',
    collection: 'users',
    foreignKey: 'userId',
    references: 'id'
  }
}
```

**hasMany** - One-to-many relationship
```typescript
relations: {
  purchases: {
    type: 'hasMany',
    collection: 'purchases',
    foreignKey: 'userId',
    references: 'id'
  }
}
```

**hasOne** - One-to-one relationship
```typescript
relations: {
  profile: {
    type: 'hasOne',
    collection: 'profiles',
    foreignKey: 'userId',
    references: 'id'
  }
}
```

### Nested Joins

Access deeply nested related data using dot notation:

```typescript
const OrderSchema = m.object({
  id: m.uuid(),
  purchaseId: m.uuid(),
  status: m.string(),
});

const OrderResponseSchema = m.object({
  id: m.uuid(),
  purchaseId: m.uuid(),
  status: m.string(),
  // Nested join: order → purchase → user
  userEmail: m.join('purchase.user', 'email'),
  userName: m.join('purchase.user', 'name'),
});

export const orders = defineCollection({
  name: "orders",
  schema: OrderSchema,
  responseSchema: OrderResponseSchema,
  relations: {
    purchase: {
      type: 'belongsTo',
      collection: 'purchases',
      foreignKey: 'purchaseId',
      references: 'id'
    }
  }
});
```

### Eager Loading

Control when related collections are loaded and cached:

```typescript
configureSymulate({
  collections: {
    eagerLoading: true,  // Load all related collections recursively
    persistence: {
      mode: "local"
    }
  }
});
```

- **`eagerLoading: true`** - Load all related collections recursively when any collection is first accessed. All data is cached together for maximum performance.
- **`eagerLoading: false`** (default) - Only load related collections when they're explicitly requested. More memory efficient.

### Foreign Key Integrity

Symulate automatically ensures referential integrity:

- Collections are seeded in dependency order (referenced collections first)
- Foreign key values always reference existing records
- No orphaned references
- Seed generation respects relation constraints

```typescript
// Seed order is automatic:
// 1. Users collection seeded first (no dependencies)
// 2. Purchases collection seeded with valid userId references
// 3. Orders collection seeded with valid purchaseId references
```

### How Joins Work

**Important**: Joined fields are resolved at query time, not stored in the database.

- Storage schema defines what's persisted (e.g., `userId`)
- Response schema defines what's returned (e.g., `userId` + `userEmail`)
- Joins happen when data is requested, keeping data normalized
- No data duplication - changes to user email automatically reflect in all purchases

## Database Table Schemas

Auto-generate collection schemas from your database tables - full type safety with zero manual mapping.

### Basic Usage

```typescript
import { dbTable, defineCollection } from '@symulate/sdk';
import type { Database } from './types/database'; // Generated by Supabase

// Auto-generate schema from database table
const UserSchema = dbTable('users');

export const users = defineCollection<Database['public']['Tables']['users']['Row']>({
  name: "users",
  basePath: "/api/users",
  schema: UserSchema,
  seedCount: 100,
});
```

### Selective Fields

Only include specific fields or exclude sensitive ones:

```typescript
// Only include specific fields
const PublicUserSchema = dbTable('users', {
  only: ['id', 'name', 'email', 'avatar']
});

// Exclude sensitive fields
const SafeUserSchema = dbTable('users', {
  exclude: ['password_hash', 'reset_token', 'internal_notes']
});
```

### Extend with Virtual Fields

Add computed or virtual fields that don't exist in the database:

```typescript
const UserSchema = dbTable('users', {
  exclude: ['password_hash']
}).extend({
  // Virtual fields
  fullName: m.string(),
  isActive: m.boolean(),
  lastLoginRelative: m.string(),
  // Computed fields
  purchaseCount: m.number({ min: 0, max: 100 }),
});

export type User = Infer<typeof UserSchema>;
```

### Combined Approach

Mix selective fields with extensions:

```typescript
const ProductSchema = dbTable('products', {
  only: ['id', 'name', 'price', 'category_id']
}).extend({
  categoryName: m.join('category', 'name'),  // Join with relations
  inStock: m.boolean(),
  rating: m.number({ min: 1, max: 5 }),
});
```

### Type Safety

TypeScript automatically catches database schema changes:

```typescript
// If you remove 'email' from database, this will show TypeScript error
const schema = dbTable('users', {
  only: ['id', 'name', 'email']  // ❌ Error: 'email' does not exist on type 'users'
});

// If database type changes from string to number
const user = await users.getById('123');
user.age.toUpperCase(); // ❌ Error: Property 'toUpperCase' does not exist on type 'number'
```

### Nullability & Optional Fields

Nullable database columns automatically become optional TypeScript fields:

```typescript
// Database schema:
// users table
//   id: uuid (not null)
//   name: text (not null)
//   bio: text (nullable)
//   avatar: text (nullable)

const UserSchema = dbTable('users');

// Generated TypeScript type:
type User = {
  id: string;
  name: string;
  bio?: string;      // Optional because nullable in DB
  avatar?: string;   // Optional because nullable in DB
}
```

**Mock Data Generation**:
- Optional fields have a 25% chance of being `undefined`
- 75% chance they'll have realistic generated values
- Applies to both Faker.js and AI generation modes
- Tests your frontend's handling of missing data

### Working with Relations

Combine `dbTable` with relations for fully typed, interconnected collections:

```typescript
const UserSchema = dbTable('users');
const PurchaseSchema = dbTable('purchases');

const PurchaseResponseSchema = dbTable('purchases').extend({
  userEmail: m.join('user', 'email'),
  userName: m.join('user', 'name'),
});

export const purchases = defineCollection({
  name: "purchases",
  schema: PurchaseSchema,
  responseSchema: PurchaseResponseSchema,
  relations: {
    user: {
      type: 'belongsTo',
      collection: 'users',
      foreignKey: 'user_id',
      references: 'id'
    }
  }
});
```

### Benefits

✅ **Zero Manual Mapping** - Schema syncs automatically with database
✅ **Type Safety** - TypeScript errors when database changes
✅ **Selective Fields** - Only expose what you need
✅ **Virtual Fields** - Extend with computed properties
✅ **Nullability Handling** - Optional fields map correctly
✅ **Realistic Testing** - Optional fields sometimes undefined (25% chance)

## Configuration Options

```typescript
configureSymulate({
  // API Keys (choose one):
  openaiApiKey: string,          // BYOK: Your OpenAI API key
  openaiModel: string,           // BYOK: OpenAI model (default: "gpt-4o-mini")
  symulateApiKey: string,        // Platform API key (sym_live_xxx)

  // Optional:
  projectId: string,             // Project ID (required for platform)
  demoApiKey: string,            // Demo API key (sym_demo_xxx) for pre-generated demos
  backendBaseUrl: string,        // Real backend URL for production
  environment: "development" | "production",  // Default: "development"
  cacheEnabled: boolean,         // Enable caching (default: true)
  persistentCache: boolean,      // Enable localStorage in browser (default: false)
  generateMode: "ai" | "faker" | "auto",  // Default: "auto"
  fakerSeed: number,             // Seed for deterministic Faker.js
  language: string,              // Language for AI-generated data (e.g., "en", "de")
  regenerateOnConfigChange: boolean,  // Regenerate when endpoint config changes (default: true)

  // Collections:
  collections: {
    branch: string,              // Branch name for data isolation
    eagerLoading: boolean,       // Load related collections recursively (default: false)
    persistence: {
      mode: "memory" | "local" | "cloud",
      filePath: string,          // File path for Node.js (default: ".symulate-data.json")
    },
  },
});
```

## Schema Builder

### Basic Types

```typescript
const schema = m.object({
  id: m.uuid(),
  text: m.string(),
  count: m.number({ min: 0, max: 100 }),
  active: m.boolean(),
  createdAt: m.date(),
  email: m.email(),
  website: m.url(),
  phone: m.phoneNumber(),
});
```

### Faker.js Fields

```typescript
const person = m.object({
  fullName: m.person.fullName(),
  firstName: m.person.firstName(),
  lastName: m.person.lastName(),
  jobTitle: m.person.jobTitle(),

  userName: m.internet.userName(),
  avatar: m.internet.avatar(),

  street: m.location.street(),
  city: m.location.city(),
  state: m.location.state(),
  zipCode: m.location.zipCode(),
  country: m.location.country(),

  productName: m.commerce.productName(),
  department: m.commerce.department(),
  price: m.commerce.price(),

  word: m.lorem.word(),
  sentence: m.lorem.sentence(),
  paragraph: m.lorem.paragraph(),
});
```

### Nested Objects & Arrays

```typescript
const user = m.object({
  id: m.uuid(),
  name: m.person.fullName(),
  address: m.object({
    street: m.location.street(),
    city: m.location.city(),
  }),
  tags: m.array(m.string()),
});
```

## Mock Configuration

```typescript
mock: {
  count: 10,                    // Generate array of 10 items
  instruction: "Generate premium users with verified accounts",
  delay: 500,                   // Simulate network latency (ms)
  metadata: {                   // Contextual data for AI
    industry: "Technology",
    region: "North America"
  }
}
```

## Parameters

Define typed parameters for your endpoints:

```typescript
const getUserById = defineEndpoint({
  path: "/api/users/:id",
  method: "GET",
  params: [
    {
      name: "id",
      location: "path",          // "path", "query", "header", or "body"
      required: true,
      schema: m.uuid(),
    },
  ],
  schema: UserSchema,
});

// Usage
const user = await getUserById({ id: "123e4567-..." });
```

## CI/CD Example

Use Faker mode for fast, deterministic tests:

```typescript
// jest.setup.ts
import { configureSymulate } from "@symulate/sdk";

configureSymulate({
  generateMode: "faker",
  fakerSeed: 12345,              // Same data every test run
  environment: "development",
});
```

```yaml
# .github/workflows/test.yml
- name: Run tests
  run: npm test
  env:
    NODE_ENV: development
```

## CLI Commands

### Collections Management

```bash
# List all collections
npx symulate collections list

# Delete specific collection
npx symulate collections delete -n products

# Pre-generate all collections
npx symulate collections pregenerate
```

### Cache Management

```bash
# View cached data
npx symulate cache

# Clear all cache
npx symulate regenerate

# Clear specific hash
npx symulate regenerate --hash abc123xyz
```

### OpenAPI Generation

```bash
# Generate OpenAPI spec
npx symulate openapi -o api-spec.json
```

### Database Schema Import

```bash
# Import database schema from platform
npx symulate import-schema

# Import specific schema by name
npx symulate import-schema --schema-name auth
```

## Database Schema Sync

Reference backend database types directly:

```typescript
import type { User, Post } from "./types/database";

const getUser = defineEndpoint<User>({
  path: "/api/users/:id",
  method: "GET",
  schema: m.object({
    id: m.db("users.id"),
    name: m.db("users.name", "German names"),
    email: m.db("users.email"),
    created_at: m.db("users.created_at"),
  }),
});
```

[Learn more about Database Schema Sync](https://symulate.dev/docs/database-schema-sync)

## Tenant Demos

Create isolated demo environments with pre-generated AI data:

```typescript
// Configure SDK with demo API key
configureSymulate({
  demoApiKey: 'sym_demo_your_key_here',
  projectId: 'proj_xxx',
});

// All calls now use pre-generated demo data
const products = await getProducts();       // Pre-generated
const users = await userCollection.list();  // Pre-generated
```

[Learn more about Tenant Demos](https://symulate.dev/docs/tenant-demos)

## Error Handling

Define custom error responses:

```typescript
const getUser = defineEndpoint({
  path: "/api/users/:id",
  method: "GET",
  schema: UserSchema,
  errors: [
    {
      code: 404,
      description: "User not found",
      schema: m.object({
        error: m.object({
          message: m.string(),
          code: m.string(),
        }),
      }),
      failNow: true, // Simulate this error in mock mode
    },
  ],
});
```

## Type Validation

Automatic runtime validation in production mode:

```typescript
import { TypeValidationError } from "@symulate/sdk";

try {
  const users = await getUsers();
} catch (error) {
  if (error instanceof TypeValidationError) {
    console.error("Expected:", error.expected);
    console.error("Received:", error.received);
  }
}
```

## Environment Switching

```bash
# Development (uses mocks)
NODE_ENV=development npm run dev

# Production (uses real backend)
NODE_ENV=production npm run build
```

Or override per-endpoint:

```typescript
export const getNewFeature = defineEndpoint({
  path: "/api/new-feature",
  method: "GET",
  mode: "mock", // Force mock mode even in production
  schema: FeatureSchema,
});
```

## Documentation

- **[Full Documentation](https://symulate.dev/docs)**
- **[Quick Start Guide](https://symulate.dev/docs/quickstart)**
- **[Collections Guide](https://symulate.dev/docs/collections)**
- **[Interconnected Collections](https://symulate.dev/docs/interconnected-collections)**
- **[Database Table Schemas](https://symulate.dev/docs/database-table-schemas)**
- **[Tenant Demos Guide](https://symulate.dev/docs/tenant-demos)**
- **[CLI Reference](https://symulate.dev/docs/cli)**
- **[Database Schema Sync](https://symulate.dev/docs/database-schema-sync)**

## Community & Support

- **[Discord Community](https://discord.gg/symulate)**
- **[GitHub Discussions](https://github.com/symulate/sdk/discussions)**
- **[GitHub Issues](https://github.com/symulate/sdk/issues)**

## License

MIT License - see [LICENSE](LICENSE) for details