Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions docs/docs/00100-intro/00200-quickstarts/00175-remix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: Remix Quickstart
sidebar_label: Remix
slug: /quickstarts/remix
hide_table_of_contents: true
---

import { InstallCardLink } from "@site/src/components/InstallCardLink";
import { StepByStep, Step, StepText, StepCode } from "@site/src/components/Steps";


Get a SpacetimeDB Remix app running in under 5 minutes.

## Prerequisites

- [Node.js](https://nodejs.org/) 18+ installed
- [SpacetimeDB CLI](https://spacetimedb.com/install) installed

<InstallCardLink />

---

<StepByStep>
<Step title="Create your project">
<StepText>
Run the `spacetime dev` command to create a new project with a SpacetimeDB module and Remix client.

This will start the local SpacetimeDB server, publish your module, generate TypeScript bindings, and start the Remix development server.
</StepText>
<StepCode>
```bash
spacetime dev --template remix-ts my-remix-app
```
</StepCode>
</Step>

<Step title="Open your app">
<StepText>
Navigate to [http://localhost:5173](http://localhost:5173) to see your app running.

The `spacetime dev` command automatically configures your app to connect to SpacetimeDB via environment variables.
</StepText>
</Step>

<Step title="Explore the project structure">
<StepText>
Your project contains both server and client code using Remix with Vite.

Edit `spacetimedb/src/index.ts` to add tables and reducers. Edit `app/routes/_index.tsx` to build your UI.
</StepText>
<StepCode>
```
my-remix-app/
├── spacetimedb/ # Your SpacetimeDB module
│ └── src/
│ └── index.ts # SpacetimeDB module logic
├── app/ # Remix app
│ ├── root.tsx # Root layout with SpacetimeDB provider
│ ├── lib/
│ │ └── spacetimedb.server.ts # Server-side data fetching
│ └── routes/
│ └── _index.tsx # Home page with loader
├── src/
│ └── module_bindings/ # Auto-generated types
└── package.json
```
</StepCode>
</Step>

<Step title="Understand tables and reducers">
<StepText>
Open `spacetimedb/src/index.ts` to see the module code. The template includes a `person` table and two reducers: `add` to insert a person, and `say_hello` to greet everyone.

Tables store your data. Reducers are functions that modify data — they're the only way to write to the database.
</StepText>
<StepCode>
```typescript
import { schema, table, t } from 'spacetimedb/server';

export const spacetimedb = schema(
table(
{ name: 'person', public: true },
{
name: t.string(),
}
)
);

spacetimedb.reducer('add', { name: t.string() }, (ctx, { name }) => {
ctx.db.person.insert({ name });
});

spacetimedb.reducer('say_hello', (ctx) => {
for (const person of ctx.db.person.iter()) {
console.info(`Hello, ${person.name}!`);
}
console.info('Hello, World!');
});
```
</StepCode>
</Step>

<Step title="Test with the CLI">
<StepText>
Use the SpacetimeDB CLI to call reducers and query your data directly.
</StepText>
<StepCode>
```bash
# Call the add reducer to insert a person
spacetime call my-remix-app add Alice

# Query the person table
spacetime sql my-remix-app "SELECT * FROM person"
name
---------
"Alice"

# Call say_hello to greet everyone
spacetime call my-remix-app say_hello

# View the module logs
spacetime logs my-remix-app
2025-01-13T12:00:00.000000Z INFO: Hello, Alice!
2025-01-13T12:00:00.000000Z INFO: Hello, World!
```
</StepCode>
</Step>

<Step title="Understand server-side rendering">
<StepText>
The SpacetimeDB SDK works both server-side and client-side. The template uses Remix loaders for SSR:

- **Loader**: Fetches initial data from SpacetimeDB during server rendering
- **Client**: Maintains a real-time WebSocket connection for live updates

The `app/lib/spacetimedb.server.ts` file provides a utility for server-side data fetching.
</StepText>
<StepCode>
```tsx
// app/lib/spacetimedb.server.ts
import { DbConnection } from '../../src/module_bindings';

export async function fetchPeople() {
return new Promise((resolve, reject) => {
const connection = DbConnection.builder()
.withUri(process.env.SPACETIMEDB_HOST!)
.withModuleName(process.env.SPACETIMEDB_DB_NAME!)
.onConnect(conn => {
conn.subscriptionBuilder()
.onApplied(() => {
const people = Array.from(conn.db.person.iter());
conn.disconnect();
resolve(people);
})
.subscribe('SELECT * FROM person');
})
.build();
});
}
```
</StepCode>
</Step>

<Step title="Use loaders and hooks for data">
<StepText>
Use Remix loaders to fetch initial data server-side, then React hooks for real-time updates. The loader data is passed to the component and displayed immediately while the client connects.
</StepText>
<StepCode>
```tsx
// app/routes/_index.tsx
import { useLoaderData } from '@remix-run/react';
import { tables, reducers } from '../../src/module_bindings';
import { useTable, useReducer } from 'spacetimedb/react';
import { fetchPeople } from '../lib/spacetimedb.server';

export async function loader() {
const people = await fetchPeople();
return { initialPeople: people };
}

export default function Index() {
const { initialPeople } = useLoaderData<typeof loader>();

// Real-time data from WebSocket subscription
const [people, isLoading] = useTable(tables.person);
const addPerson = useReducer(reducers.add);

// Use server data until client is connected
const displayPeople = isLoading ? initialPeople : people;

return (
<ul>
{displayPeople.map((person, i) => <li key={i}>{person.name}</li>)}
</ul>
);
}
```
</StepCode>
</Step>
</StepByStep>

## Next steps

- See the [Chat App Tutorial](/tutorials/chat-app) for a complete example
- Read the [TypeScript SDK Reference](/sdks/typescript) for detailed API docs
5 changes: 5 additions & 0 deletions templates/remix-ts/.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Remix with TypeScript server",
"client_lang": "typescript",
"server_lang": "typescript"
}
1 change: 1 addition & 0 deletions templates/remix-ts/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
../../licenses/apache2.txt
49 changes: 49 additions & 0 deletions templates/remix-ts/app/lib/spacetimedb.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { DbConnection, Person } from '../../src/module_bindings';
import type { Infer } from 'spacetimedb';

const HOST = process.env.SPACETIMEDB_HOST ?? 'wss://maincloud.spacetimedb.com';
const DB_NAME = process.env.SPACETIMEDB_DB_NAME ?? 'remix-ts';

export type PersonData = Infer<typeof Person>;

/**
* Fetches the initial list of people from SpacetimeDB.
* This function is designed for use in Remix loaders.
*
* It establishes a WebSocket connection, subscribes to the person table,
* waits for the initial data, and then disconnects.
*/
export async function fetchPeople(): Promise<PersonData[]> {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('SpacetimeDB connection timeout'));
}, 10000);

const connection = DbConnection.builder()
.withUri(HOST)
.withModuleName(DB_NAME)
.onConnect(conn => {
// Subscribe to all people
conn
.subscriptionBuilder()
.onApplied(() => {
clearTimeout(timeoutId);
// Get all people from the cache
const people = Array.from(conn.db.person.iter());
conn.disconnect();
resolve(people);
})
.onError((_ctx, error) => {
clearTimeout(timeoutId);
conn.disconnect();
reject(error);
})
.subscribe('SELECT * FROM person');
})
.onConnectError((_ctx, error) => {
clearTimeout(timeoutId);
reject(error);
})
.build();
});
}
88 changes: 88 additions & 0 deletions templates/remix-ts/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useMemo, useState, useEffect } from 'react';
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from '@remix-run/react';
import type { LinksFunction } from '@remix-run/node';
import { SpacetimeDBProvider } from 'spacetimedb/react';
import { DbConnection, ErrorContext } from '../src/module_bindings';
import { Identity } from 'spacetimedb';

const HOST =
import.meta.env.VITE_SPACETIMEDB_HOST ?? 'wss://maincloud.spacetimedb.com';
const DB_NAME = import.meta.env.VITE_SPACETIMEDB_DB_NAME ?? 'remix-ts';

const onConnect = (_conn: DbConnection, identity: Identity, token: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('auth_token', token);
}
console.log(
'Connected to SpacetimeDB with identity:',
identity.toHexString()
);
};

const onDisconnect = () => {
console.log('Disconnected from SpacetimeDB');
};

const onConnectError = (_ctx: ErrorContext, err: Error) => {
console.log('Error connecting to SpacetimeDB:', err);
};

export const links: LinksFunction = () => [];

function Providers({ children }: { children: React.ReactNode }) {
const [isClient, setIsClient] = useState(false);

useEffect(() => {
setIsClient(true);
}, []);

const connectionBuilder = useMemo(() => {
if (typeof window === 'undefined') return null;
return DbConnection.builder()
.withUri(HOST)
.withModuleName(DB_NAME)
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect(onConnect)
.onDisconnect(onDisconnect)
.onConnectError(onConnectError);
}, []);

// During SSR or before hydration, render children without provider
if (!isClient || !connectionBuilder) {
return <>{children}</>;
}

return (
<SpacetimeDBProvider connectionBuilder={connectionBuilder}>
{children}
</SpacetimeDBProvider>
);
}

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Providers>{children}</Providers>
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App() {
return <Outlet />;
}
Loading
Loading