Skip to content

Reading data

A SvelteKit page runs twice: on the server (SSR), then in the browser. remult’s repo(E) is not the same in both:

  • in the browser, it calls your API: API rules apply (allowApiRead, apiPrefilter, includeInApi).
  • on the server, it reads the database directly: no API rules.

Four ways to read safely. All four apply your API rules:

Data in the SSR HTML Files After a repo(E) write
ssr = false no SSR at all 2 (+page.ts) re-fetch yourself
$effect no, page renders empty 1 re-fetch yourself
loadRepo yes 2 (+page.ts) invalidateAll()
queryRepo (experimental) yes 1 automatic
+page.ts
export const ssr = false
+page.svelte
<script lang="ts">
import { repo } from 'remult'
import { Task } from '#lib/Task.js'
</script>
{#await repo(Task).find() then tasks}
{#each tasks as task (task.id)}
<li>{task.title}</li>
{/each}
{/await}

The page never runs on the server, so repo(E) always goes through the API. The cost: no HTML until the JS loads, nothing for crawlers or link previews. Put it in a +layout.ts for a whole section.

<script lang="ts">
import { repo } from 'remult'
import { Task } from '#lib/Task.js'
let { params } = $props()
let tasks = $state<Task[]>([])
$effect(() => {
repo(Task)
.find({ where: { slug: params.slug } })
.then((rows) => (tasks = rows))
})
</script>
{#each tasks as task (task.id)}
<li>{task.title}</li>
{/each}

$effect never runs on the server: SSR sends the page without the data, the browser fills it in. ff is built the same way, with loading state, paging and writes wired for you.

+page.ts
import { loadRepo } from 'firstly/svelte'
import { Task } from '#lib/Task.js'
import type { PageLoad } from './$types'
export const load = loadRepo(async (repoClient, { params }) => {
const tasks = await repoClient(Task).find({ where: { slug: params.slug } })
return { tasks }
}) satisfies PageLoad
+page.svelte
<script lang="ts">
let { data } = $props()
</script>
{#each data.tasks as task (task.id)}
<li>{task.title}</li>
{/each}

repoClient is a repo that goes through your API with event.fetch, on the server too, as the current user. Hydration reuses the SSR response: the browser sends no second query. After a write, call invalidateAll() to run the load again.

loadRepo also works in +page.server.ts (satisfies PageServerLoad types cookies and locals). There, return plain objects: Kit serializes server data. Under the hood it’s repoFetch(event.fetch), use it directly when you need more control.

All the plumbing is done for you: SSR, API rules and writes that refresh, in one file.

<script lang="ts">
import { repo } from 'remult'
import { queryRepo } from 'firstly/svelte/query'
import { Task } from '#lib/Task.js'
let { params } = $props()
let tasks = $derived(await queryRepo(Task).find({ where: { slug: params.slug } }))
</script>
{#each tasks as task (task.id)}
<button onclick={() => repo(Task).update(task.id, { done: true })}>{task.title}</button>
{/each}
  • SSR reads through your API as the current user, hydration sends no query.
  • When params.slug changes, it reads again.
  • Plain repo(E) writes in the browser refresh the queryRepo reads of E on screen. Call .refresh() for what it can’t see: BackendMethods, included entities, other users’ writes.
  • Also findFirst(where) and count(where).

It needs Kit’s remote functions and Svelte’s await:

vite.config.ts
sveltekit({
compilerOptions: { experimental: { async: true } },
experimental: { remoteFunctions: true },
})

How it works: a class can’t be sent to the server, so queryRepo sends the entity key to a remote query. The server finds the class again, reads through your API with event.fetch and sends JSON back, which queryRepo turns into Task instances.