tinykit provides three special imports for handling data in your apps:
Import Purpose $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
Type Description Example Value textSingle line text "Welcome to My App"textareaMulti-line text "A longer description..."numberNumeric value 42booleanTrue/false toggle truejsonStructured data ["item1", "item2"]
Creating Content Fields
The AI creates content fields when it builds your app. You can also:
Ask the AI : “Add a content field for the footer copyright text”
Use the Content tab : Add fields directly in the builder
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 >
< 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 >
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 , 8 px );
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.