Skip to main content
tinykit provides three special imports for handling data in your apps:
ImportPurpose
$dataDatabase collections with realtime updates
$contentEditable CMS text fields
$tinykitProxy for fetching external APIs

Database ($data)

Store and retrieve data with realtime subscriptions. Data is stored in Pocketbase and syncs automatically across all connected clients.

Basic Usage

<script>
  import data from '$data'

  let todos = $state([])
  let loading = $state(true)

  // Subscribe to realtime updates
  $effect(() => {
    return data.todos.subscribe(items => {
      todos = items
      loading = false
    })
  })
</script>

{#if loading}
  <p>Loading...</p>
{:else}
  {#each todos as todo (todo.id)}
    <p>{todo.title}</p>
  {/each}
{/if}
Always return the subscription from $effect() for proper cleanup. This prevents memory leaks when components unmount.

CRUD Operations

// Create a new record
await data.todos.create({
  title: 'Buy groceries',
  completed: false
})

// Update a record by ID
await data.todos.update('abc123', {
  completed: true
})

// Delete a record by ID
await data.todos.delete('abc123')
All operations trigger realtime updates—any subscribed component will update automatically.

Complete Example

<script>
  import data from '$data'

  let todos = $state([])
  let loading = $state(true)
  let new_title = $state('')

  $effect(() => {
    return data.todos.subscribe(items => {
      todos = items
      loading = false
    })
  })

  async function add_todo() {
    if (!new_title.trim()) return
    await data.todos.create({
      title: new_title,
      completed: false
    })
    new_title = ''
  }

  async function toggle_todo(todo) {
    await data.todos.update(todo.id, {
      completed: !todo.completed
    })
  }

  async function delete_todo(id) {
    await data.todos.delete(id)
  }
</script>

<input bind:value={new_title} placeholder="New todo" />
<button onclick={add_todo}>Add</button>

{#each todos as todo (todo.id)}
  <div>
    <input
      type="checkbox"
      checked={todo.completed}
      onchange={() => toggle_todo(todo)}
    />
    <span>{todo.title}</span>
    <button onclick={() => delete_todo(todo.id)}>Delete</button>
  </div>
{/each}

Creating Collections

The AI creates collections when you ask for data storage. You can also create them in the Data tab or ask the AI directly:
Add a collection for storing recipes with title, ingredients, and instructions

Content Fields ($content)

Editable text values that non-developers can change without touching code. Perfect for headlines, descriptions, and labels.

Basic Usage

<script>
  import content from '$content'
</script>

<h1>{content.hero_title}</h1>
<p>{content.hero_description}</p>
<button>{content.cta_button_text}</button>
Content field names are automatically converted to snake_case:
  • “Hero Title” → content.hero_title
  • “CTA Button Text” → content.cta_button_text

Default Values

Always provide fallbacks for robustness:
<h1>{content.hero_title || 'Welcome'}</h1>

Field Types

TypeDescriptionExample Value
textSingle line text"Welcome to My App"
textareaMulti-line text"A longer description..."
numberNumeric value42
booleanTrue/false toggletrue
jsonStructured data["item1", "item2"]

Creating Content Fields

The AI creates content fields when it builds your app. You can also:
  1. Ask the AI: “Add a content field for the footer copyright text”
  2. Use the Content tab: Add fields directly in the builder
  3. Edit values: Change text in the Content tab without touching code

External APIs ($tinykit)

Fetch data from external APIs without CORS issues. The proxy routes requests through your server.

Fetching JSON

<script>
  import { proxy } from '$tinykit'

  let data = $state(null)
  let loading = $state(true)

  $effect(() => {
    load_data()
  })

  async function load_data() {
    data = await proxy.json('https://api.example.com/data')
    loading = false
  }
</script>

Fetching Text (RSS, HTML, XML)

<script>
  import { proxy } from '$tinykit'

  let rss_content = $state('')

  $effect(() => {
    load_rss()
  })

  async function load_rss() {
    rss_content = await proxy.text('https://hnrss.org/frontpage')
  }
</script>

Media URLs

For audio, images, and other media that need a direct URL:
<script>
  import { proxy } from '$tinykit'

  let podcast_url = 'https://example.com/episode.mp3'
</script>

<audio src={proxy.url(podcast_url)} controls />
<img src={proxy.url('https://example.com/image.jpg')} alt="Remote image" />

Raw Fetch

For full control over the request:
import { proxy } from '$tinykit'

const response = await proxy('https://api.example.com/data')
const json = await response.json()

When to Use Proxy

Use the proxy when fetching from external domains that would block direct browser requests:

Use Proxy

  • RSS feeds
  • External APIs without CORS headers
  • Scraping web pages
  • Remote media files (audio/video)

Don't Need Proxy

  • Your own $data collections
  • APIs with proper CORS headers
  • CDN resources (images, scripts)
  • Same-origin requests

Common Patterns

Loading States

Always show loading indicators:
<script>
  import data from '$data'

  let items = $state([])
  let loading = $state(true)

  $effect(() => {
    return data.items.subscribe(records => {
      items = records
      loading = false
    })
  })
</script>

{#if loading}
  <div class="loading">Loading...</div>
{:else if items.length === 0}
  <div class="empty">No items yet</div>
{:else}
  {#each items as item (item.id)}
    <div>{item.name}</div>
  {/each}
{/if}

Filtering and Sorting

Use $derived.by() for computed lists:
<script>
  import data from '$data'

  let todos = $state([])
  let show_completed = $state(false)

  // Use $derived.by() for callbacks/filters
  let visible_todos = $derived.by(() =>
    todos.filter(t => show_completed || !t.completed)
  )

  // Use [...] spread before sort (sort mutates the array)
  let sorted_todos = $derived.by(() =>
    [...visible_todos].sort((a, b) => a.created - b.created)
  )
</script>
Common mistake: $derived(todos.filter(...)) won’t work. Use $derived.by(() => todos.filter(...)) for callbacks.

Combining Data and Content

<script>
  import data from '$data'
  import content from '$content'

  let recipes = $state([])
  let loading = $state(true)

  $effect(() => {
    return data.recipes.subscribe(items => {
      recipes = items
      loading = false
    })
  })
</script>

<h1>{content.page_title}</h1>
<p>{content.page_description}</p>

{#each recipes as recipe (recipe.id)}
  <article>
    <h2>{recipe.title}</h2>
    <p>{recipe.description}</p>
  </article>
{/each}

{#if recipes.length === 0 && !loading}
  <p>{content.empty_state_message}</p>
{/if}

Error Handling

<script>
  import { proxy } from '$tinykit'

  let data = $state(null)
  let error = $state(null)
  let loading = $state(true)

  $effect(() => {
    fetch_data()
  })

  async function fetch_data() {
    try {
      data = await proxy.json('https://api.example.com/data')
    } catch (e) {
      error = 'Failed to load data'
    } finally {
      loading = false
    }
  }
</script>

{#if loading}
  <p>Loading...</p>
{:else if error}
  <p class="error">{error}</p>
  <button onclick={fetch_data}>Retry</button>
{:else}
  <!-- Display data -->
{/if}

Design Fields (CSS Variables)

While not an import, design fields work similarly—the AI creates CSS variables that you reference in your styles:
.card {
  background: var(--card-background, #ffffff);
  border-radius: var(--card-radius, 8px);
  color: var(--body-text-color, #333333);
}
Always include fallback values: var(--name, fallback). This ensures your app works even if a design field hasn’t been created yet.
Design fields appear in the Design tab where you can adjust colors, fonts, spacing, and more with visual editors.