> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tinykit.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Data & Content

> Working with data, content fields, and external APIs in tinykit

tinykit provides three special imports for handling data in your apps:

| Import     | Purpose                                    |
| ---------- | ------------------------------------------ |
| `$data`    | Database collections with realtime updates |
| `$content` | Editable CMS text fields                   |
| `$tinykit` | Proxy 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

```svelte theme={null}
<script>
  import data from '$data'

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

  // Subscribe to realtime updates
  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}
```

### CRUD Operations

```javascript theme={null}
// 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

```svelte theme={null}
<script>
  import data from '$data'

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

  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

```svelte theme={null}
<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:

```svelte theme={null}
<h1>{content.hero_title || 'Welcome'}</h1>
```

### Field Types

| Type       | Description                  | Example Value               |
| ---------- | ---------------------------- | --------------------------- |
| `text`     | Single line text             | `"Welcome to My App"`       |
| `textarea` | Multi-line text              | `"A longer description..."` |
| `number`   | Numeric value                | `42`                        |
| `boolean`  | True/false toggle            | `true`                      |
| `image`    | Image upload                 | `"/path/to/image.png"`      |
| `markdown` | Rich text (rendered as HTML) | `"# Heading\n\nParagraph"`  |

### 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

```svelte theme={null}
<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)

```svelte theme={null}
<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:

```svelte theme={null}
<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:

```javascript theme={null}
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:

<CardGroup cols={2}>
  <Card title="Use Proxy" icon="check">
    * RSS feeds
    * External APIs without CORS headers
    * Scraping web pages
    * Remote media files (audio/video)
  </Card>

  <Card title="Don't Need Proxy" icon="xmark">
    * Your own `$data` collections
    * APIs with proper CORS headers
    * CDN resources (images, scripts)
    * Same-origin requests
  </Card>
</CardGroup>

***

## Common Patterns

### Loading States

Always show loading indicators:

```svelte theme={null}
<script>
  import data from '$data'

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

  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:

```svelte theme={null}
<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>
```

<Warning>
  Common mistake: `$derived(todos.filter(...))` won't work. Use `$derived.by(() => todos.filter(...))` for callbacks.
</Warning>

### Combining Data and Content

```svelte theme={null}
<script>
  import data from '$data'
  import content from '$content'

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


  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

```svelte theme={null}
<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:

```css theme={null}
.card {
  background: var(--card-background, #ffffff);
  border-radius: var(--card-radius, 8px);
  color: var(--body-text-color, #333333);
}
```

<Tip>
  Always include fallback values: `var(--name, fallback)`. This ensures your app works even if a design field hasn't been created yet.
</Tip>

Design fields appear in the Design tab where you can adjust colors, fonts, spacing, and more with visual editors.
