@sveltebase/instant
v0.4.6
Published
Typed InstantDB helpers for Svelte and SvelteKit.
Downloads
1,823
Readme
@sveltebase/instant
Typed InstantDB helpers for Svelte and SvelteKit.
This package is mainly for giving @instantdb/svelte a clean SvelteKit SSR flow. It helps you:
- create app-local helpers around your InstantDB client
- read the signed-in user from the server cookie
- initialize Instant auth on the client with that server user
- handle auth syncing and authenticated server queries through one SvelteKit endpoint
- prefetch query data during SSR
- hydrate that same data into
useQueryso the first render is already filled
If you are using InstantDB inside SvelteKit and want the first server render and later client navigations to work nicely together, this package is the missing glue.
Install
Add the package with Bun:
bun add @sveltebase/instantYou will also need the peer dependencies used by this package:
bun add @instantdb/svelte svelteIf you are using the SvelteKit server handler from this package, install SvelteKit too:
bun add @sveltejs/kitWhat it exports
createInstantHelpersuseQueryprefetchQuerycreateInstantHandlerparseUser
Recommended SvelteKit setup
The recommended setup is:
- create your local Instant helpers in
$lib/db/index.ts - read the current user in
+layout.server.ts - initialize auth in
+layout.svelte - create one catch-all endpoint at
/api/instant/[...action]/+server.ts - define each page query once in
_components/query.ts - use that same query in both
+page.tsand+page.svelte
This gives you a simple SSR flow:
- on the first request, the server loads data through your Instant API route
- the page renders with real data immediately
- after hydration,
useQuerycontinues on the client - on later navigations, SvelteKit can preload the page on hover, so data starts loading on the client before the navigation completes
1. Create $lib/db/index.ts
Create your Instant client once, then bind the helpers to it.
import { init } from "@instantdb/svelte";
import { createInstantHelpers } from "@sveltebase/instant";
import schema from "./instant.schema";
export const db = init({
appId: "YOUR_INSTANT_APP_ID",
schema
});
export const { auth, useQuery, prefetchQuery } = createInstantHelpers(db);This gives you app-local helpers already connected to your db instance, so the rest of your app can just import from $lib/db.
2. Load the user in +layout.server.ts
Use parseUser to read the Instant user from cookies on the server.
import { parseUser } from "@sveltebase/instant";
export async function load({ cookies }) {
const user = parseUser(cookies.getAll());
return { user };
}This reads the instant_user cookie and makes the user available to your root layout.
3. Initialize auth in +layout.svelte
Initialize the auth helper with the user returned from the server.
<script lang="ts">
import { auth } from "$lib/db";
import type { LayoutData } from "./$types";
let { data }: { data: LayoutData } = $props();
auth.init(() => data.user);
</script>
<slot />This is important for SSR. The server already knows who the user is, so the client should start with the same user immediately instead of discovering auth state later.
4. Create /api/instant/[...action]/+server.ts
Create the unified Instant handler and load adminDb from $lib/server/db.
import { createInstantHandler } from "@sveltebase/instant";
import { adminDb } from "$lib/server/db";
export const POST = createInstantHandler(adminDb);This route handles the server side of the package:
- auth state syncing
- authenticated Instant queries during SSR
adminDb should be your server-only Instant admin client. The handler uses it to create a user-scoped database from the current signed-in user's token and run the query as that user.
5. Create a shared query in _components/query.ts
For each page, define the query once and reuse it everywhere.
For example, if your route is src/routes/posts, create:
export const postsQuery = () => ({
posts: {}
});A common place for that is:
src/routes/posts/_components/query.tsThis keeps your page load function and your Svelte component in sync because they both use the exact same query definition.
6. Use the same query in +page.ts
In the page load function, prefetch the query for SSR.
import { prefetchQuery } from "$lib/db";
import { postsQuery } from "./_components/query";
export async function load({ fetch }) {
const posts = await prefetchQuery(postsQuery(), fetch);
return { posts };
}On the first request, this runs on the server. prefetchQuery sends the query to the package's Instant route, which executes the query with the current authenticated user and returns JSON for SSR.
7. Use the same query in +page.svelte
In the page component, pass the server-loaded data into useQuery.
<script lang="ts">
import { useQuery } from "$lib/db";
import type { PageData } from "./$types";
import { postsQuery } from "./_components/query";
let { data }: { data: PageData } = $props();
const posts = useQuery(postsQuery(), () => data.posts);
</script>
{#if posts.isLoading}
<p>Loading...</p>
{:else if posts.data}
<ul>
{#each posts.data.posts as post}
<li>{post.title}</li>
{/each}
</ul>
{/if}This is the key SSR pattern:
+page.tsgets the initial data on the server+page.sveltereceives that data immediatelyuseQuerystarts from that initial value instead of rendering an empty state first- after that, Instant continues as a live client-side subscription
How SSR works
Here is the full request flow:
First page load
When someone opens the page directly:
- SvelteKit runs
+layout.server.ts parseUser(cookies.getAll())reads the current user from the cookie- SvelteKit runs
+page.ts prefetchQuery(...)sends the query to/api/instant/querycreateInstantHandler(adminDb)handles that request on the server- the handler reads the same auth cookie, creates a user-scoped database, and runs the query
- the result is returned to
+page.ts - the page is rendered on the server with real data
+layout.sveltecallsauth.init(data.user)+page.sveltecallsuseQuery(..., () => data.posts)so hydration starts with the already-fetched data
That means the first HTML response is already filled with the query result.
Later navigations
After the app is running in the browser, the flow is smoother:
- SvelteKit can start loading the next page on hover
- that means
+page.tscan begin before the click finishes - from there, data loading happens client-side for faster transitions
- the page still uses the same shared query definition
useQuerykeeps the page reactive after navigation completes
So the pattern gives you both:
- SSR data on the first request
- fast client-side navigations afterward
Full example
$lib/db/index.ts
import { init } from "@instantdb/svelte";
import { createInstantHelpers } from "@sveltebase/instant";
import schema from "./instant.schema";
export const db = init({
appId: "YOUR_INSTANT_APP_ID",
schema
});
export const { auth, useQuery, prefetchQuery } = createInstantHelpers(db);src/routes/+layout.server.ts
import { parseUser } from "@sveltebase/instant";
export async function load({ cookies }) {
const user = parseUser(cookies.getAll());
return { user };
}src/routes/+layout.svelte
<script lang="ts">
import { auth } from "$lib/db";
let { data, children } = $props();
auth.init(() =>data.user);
</script>
{@render children()}
src/routes/api/instant/[...action]/+server.ts
import { createInstantHandler } from "@sveltebase/instant";
import { adminDb } from "$lib/server/db";
export const POST = createInstantHandler(adminDb);src/routes/posts/_components/query.ts
export const postsQuery = () => ({
posts: {}
});src/routes/posts/+page.ts
import { prefetchQuery } from "$lib/db";
import { postsQuery } from "./_components/query";
export async function load({ fetch }) {
const posts = await prefetchQuery(postsQuery(), fetch);
return { posts };
}src/routes/posts/+page.svelte
<script lang="ts">
import { useQuery } from "$lib/db";
import type { PageData } from "./$types";
import { postsQuery } from "./_components/query";
let { data }: { data: PageData } = $props();
const posts = useQuery(postsQuery, () => data.posts);
</script>
{#if posts.isLoading}
<p>Loading...</p>
{:else if posts.data}
<ul>
{#each posts.data.posts as post}
<li>{post.title}</li>
{/each}
</ul>
{/if}useQuery
Use useQuery inside Svelte components when you want a reactive Instant subscription.
<script lang="ts">
import { useQuery } from "$lib/db";
const posts = useQuery(() => ({
posts: {}
}));
</script>You can also disable a query by returning null:
<script lang="ts">
import { useQuery } from "$lib/db";
let enabled = false;
const posts = useQuery(() => {
if (!enabled) return null;
return {
posts: {}
};
});
</script>You may optionally provide initial data, which is what makes the SSR flow above work cleanly:
const posts = useQuery(
() => ({ posts: {} }),
() => initialPosts
);prefetchQuery
prefetchQuery is the server-friendly loader.
Use it in SvelteKit load functions when you want the first render to already contain the query result.
import { prefetchQuery } from "$lib/db";
const posts = await prefetchQuery(
{ posts: {} },
fetch
);In SSR, it requests data through the package's Instant route. In the browser, it falls back to a client-side query flow.
Auth syncing
The auth helper keeps Instant auth and your SvelteKit server in sync.
Read the current user
const user = auth.user;
const initialized = auth.initialized;Clean up
If you ever create the auth helper in a lifecycle-sensitive place, you can destroy it when no longer needed:
auth.destroy();Notes
parseUserreads the current user from theinstant_usercookie arraycreateInstantHandlerhandles both auth syncing and authenticated query execution- authenticated server queries require a signed-in user with a
refresh_token prefetchQueryreturnsnullif the server request is not successfuluseQuerythrows if the subscription returns an error
Peer dependencies
This package declares these peer dependencies:
@instantdb/sveltesvelte@sveltejs/kit(optional, only needed for the server handler)
License
ISC
