Skip to main content
If you’re already paying for Claude Pro, ChatGPT Plus, or another LLM subscription, you can generate tinykit apps directly in those chat interfaces and import them—no API key needed.

How It Works

  1. Prompt your LLM with the template below
  2. Copy the JSON response
  3. Import on the app creation screen or History tab

Snapshot Format

tinykit uses a simple JSON format:
{
  "description": "My App",
  "frontend_code": "<script>\n  // Svelte 5 code\n</script>\n\n<div>\n  <!-- HTML -->\n</div>\n\n<style>\n  /* CSS */\n</style>",
  "design": [
    {
      "id": "1",
      "name": "Primary Color",
      "css_var": "--color-primary",
      "value": "#3b82f6",
      "type": "color",
      "description": "Main brand color"
    }
  ],
  "content": [
    {
      "id": "1",
      "name": "Site Title",
      "key": "site_title",
      "value": "My App",
      "type": "text",
      "description": "Displayed in the header"
    }
  ],
  "collections": [
    {
      "name": "todos",
      "schema": [
        { "name": "title", "type": "text" },
        { "name": "completed", "type": "boolean" }
      ],
      "records": [
        { "id": "1", "title": "Example task", "completed": false }
      ]
    }
  ]
}
Only frontend_code is required. The rest are optional.

Prompt Template

Copy this into Claude, ChatGPT, or your preferred LLM:
Create a tinykit app: [DESCRIBE YOUR APP HERE]

Output valid JSON with this exact structure:

```json
{
  "description": "App name",
  "frontend_code": "SVELTE_5_COMPONENT_HERE",
  "design": [],
  "content": [],
  "collections": []
}
```

## Architecture

- Single Svelte 5 file with standard CSS in <style>
- NO Tailwind/utility classes - use semantic class names (.card, .button, .header)
- Data via `import data from '$data'` with realtime subscriptions
- Content via `import content from '$content'` for editable text
- Design via CSS variables (--kebab-case) with fallbacks
- External data via `import { proxy } from '$tinykit'`

## Svelte 5 Runes (REQUIRED)

```javascript
let count = $state(0)                                      // reactive state
let doubled = $derived(count * 2)                          // simple expressions ONLY
let filtered = $derived.by(() => items.filter(x => x.done)) // use .by() for callbacks/filters
$effect(() => { /* side effects, return cleanup fn */ })
```

- Events: `onclick={fn}` (NOT on:click). Call e.preventDefault() in handler.
- Props: `let { x } = $props()` (NOT export let)
- Bindings: `bind:value` works in Svelte 5

## Data API

```javascript
import data from '$data'

// Subscribe for realtime updates (returns unsubscribe fn)
$effect(() => data.todos.subscribe(items => { todos = items; loading = false }))

// CRUD - realtime auto-updates UI
await data.todos.create({ title: 'New' })
await data.todos.update(id, { done: true })
await data.todos.delete(id)
```

## Proxy API (for external data)

```javascript
import { proxy } from '$tinykit'

const data = await proxy.json('https://api.example.com/data')  // JSON
const rss = await proxy.text('https://hnrss.org/frontpage')    // text/RSS/XML
<audio src={proxy.url('https://example.com/audio.mp3')} />     // media URLs
```

## Responsive Layout

- Mobile-first: base styles for small screens, media queries for larger
- Stack layouts vertically by default, horizontal on wider screens
- Use relative units (%, rem, fr) not fixed px widths
- Touch-friendly: buttons/links min 44px tap target

```css
.container { padding: 1rem; }
.grid { display: flex; flex-direction: column; gap: 1rem; }

@media (min-width: 768px) {
  .container { padding: 2rem; }
  .grid { flex-direction: row; }
}
```

## Design Fields

Use CSS vars with fallbacks in code:
```css
.card { background: var(--card-background, #ffffff); }
```

Types: "color", "font", "radius", "shadow", "size", "text"
Name descriptively: "Card Background" not "Primary Color"

## Content Fields

Reference in code as content.field_key:
```svelte
<h1>{content.hero_title}</h1>
```

Types: "text", "textarea", "number", "boolean", "json"
Extract ALL user-facing text: titles, buttons, placeholders, empty states.

## Collections (Database)

- schema: array of {name, type} where type is "text", "number", "boolean", "date", "email", or "url"
- records: sample data (optional)

## Common Mistakes (AVOID)

- `$derived(items.filter(...))` → use `$derived.by(() => items.filter(...))` for callbacks
- `result.sort()` in $derived → use `[...result].sort()` (sort mutates, copy first)
- Missing return in $effect → `$effect(() => { return data.x.subscribe(...) })` for cleanup
- `on:click` → use `onclick` (Svelte 5)
- `export let x` → use `let { x } = $props()` (Svelte 5)
- No loading state → always `let loading = $state(true)`, set false in subscribe callback
- Missing fallback → `var(--x, #default)` not `var(--x)`

## UX Polish

- Use `transition:fade={{ duration: 100 }}` for dialogs, modals, and list items
- Import: `import { fade } from 'svelte/transition'`

## JSON Output Rules

- Escape newlines as \n and quotes as \" in frontend_code
- Output ONLY the JSON, no explanation

Importing

New App

  1. Go to the app creation screen
  2. Click Import from JSON
  3. Paste or upload your JSON

Existing App

  1. Open the History tab (Cmd+6)
  2. Click the upload button (↑)
  3. Select your .json file

Tips

The most common problem is improper escaping in frontend_code. Newlines must be \n and quotes must be \". If your LLM outputs code blocks inside the JSON, ask it to:
Output the frontend_code as a properly escaped JSON string,
not as a code block. Use \n for newlines and \" for quotes.
Paste your JSON into jsonlint.com before importing. Common issues:
  • Trailing commas after last array/object item
  • Unescaped quotes in strings
  • Missing closing brackets