Skip to content
Closed
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
7 changes: 7 additions & 0 deletions crates/bindings-typescript/src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from '../sdk/index';
export { Identity } from '../lib/identity';
export { ConnectionId } from '../lib/connection_id';
export { Timestamp } from '../lib/timestamp';
export { TimeDuration } from '../lib/time_duration';
export { ScheduleAt } from '../lib/schedule_at';
export { AlgebraicType, ProductType, SumType } from '../lib/algebraic_type';
31 changes: 31 additions & 0 deletions crates/bindings-typescript/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,37 @@ export default defineConfig([
esbuildOptions: commonEsbuildTweaks(),
},

// Browser bundle (IIFE for script tags): dist/browser.bundle.js
{
entry: { 'browser.bundle': 'src/browser/index.ts' },
format: ['iife'],
globalName: 'SpacetimeDB',
target: 'es2022',
outDir: 'dist',
dts: false,
sourcemap: true,
platform: 'browser',
treeshake: 'smallest',
noExternal: [/.*/],
outExtension: () => ({ js: '.js' }),
esbuildOptions: commonEsbuildTweaks(),
},

// Browser bundle ESM: dist/browser/index.mjs
{
entry: { index: 'src/browser/index.ts' },
format: ['esm'],
target: 'es2022',
outDir: 'dist/browser',
dts: false,
sourcemap: true,
clean: true,
platform: 'browser',
treeshake: 'smallest',
outExtension,
esbuildOptions: commonEsbuildTweaks(),
},

// The below minified builds are not referenced in package.json and are
// just included in the build for measuring the size impact of minification.
// It is expected that consumers of the library will run their own
Expand Down
118 changes: 118 additions & 0 deletions docs/docs/00100-intro/00200-quickstarts/00180-browser.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Browser Quickstart
sidebar_label: Browser
slug: /quickstarts/browser
hide_table_of_contents: true
pagination_next: intro/quickstarts/typescript
---

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

Get a SpacetimeDB app running in the browser with inline JavaScript.

## 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 TypeScript SpacetimeDB module.

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

<Step title="Build the client bindings">
<StepText>
The generated bindings need to be bundled into a JavaScript file that can be loaded in the browser.
</StepText>
<StepCode>
```bash
cd my-spacetime-app
npm install
npm run build
```
</StepCode>
</Step>

<Step title="Open in browser">
<StepText>
Open `index.html` directly in your browser. The app connects to SpacetimeDB and displays data in real-time.

The JavaScript code runs inline in a script tag, using the bundled `DbConnection` class.
</StepText>
<StepCode>
```html
<!-- Load the bundled bindings -->
<script src="dist/bindings.iife.js"></script>

<script>
const conn = DbConnection.builder()
.withUri('ws://localhost:3000')
.withModuleName('my-spacetime-app')
.withToken(localStorage.getItem('auth_token'))
.onConnect((conn, identity, token) => {
localStorage.setItem('auth_token', token);
console.log('Connected:', identity.toHexString());

// Subscribe to tables
conn.subscriptionBuilder()
.onApplied(() => {
for (const person of conn.db.person.iter()) {
console.log(person.name);
}
})
.subscribe(['SELECT * FROM person']);
})
.build();
</script>
```
</StepCode>
</Step>

<Step title="Call reducers">
<StepText>
Reducers are functions that modify data — they're the only way to write to the database.
</StepText>
<StepCode>
```javascript
// Call a reducer with named arguments
conn.reducers.add({ name: 'Alice' });
```
</StepCode>
</Step>

<Step title="React to changes">
<StepText>
Register callbacks to update your UI when data changes.
</StepText>
<StepCode>
```javascript
conn.db.person.onInsert((ctx, person) => {
console.log('New person:', person.name);
});

conn.db.person.onDelete((ctx, person) => {
console.log('Removed:', person.name);
});
```
</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
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ packages:
- 'templates/react-ts'
- 'templates/basic-ts'
- 'templates/vue-ts'
- 'templates/browser-ts'
- 'modules/benchmarks-ts'
- 'modules/module-test-ts'
- 'templates/chat-react-ts/spacetimedb'
Expand Down
5 changes: 5 additions & 0 deletions templates/browser-ts/.template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Script tag web app with TypeScript server",
"client_lang": "typescript",
"server_lang": "typescript"
}
1 change: 1 addition & 0 deletions templates/browser-ts/LICENSE
122 changes: 122 additions & 0 deletions templates/browser-ts/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SpacetimeDB Browser App</title>
</head>
<body
style="
font-family:
system-ui,
-apple-system,
sans-serif;
padding: 2rem;
max-width: 600px;
margin: 0 auto;
"
>
<h1>SpacetimeDB Browser App</h1>

<div style="margin-bottom: 1rem">
Status: <strong id="status" style="color: red">Disconnected</strong>
</div>

<form id="add-form" style="margin-bottom: 2rem">
<input
type="text"
id="name-input"
placeholder="Enter name"
style="padding: 0.5rem; margin-right: 0.5rem"
disabled
/>
<button type="submit" id="add-btn" style="padding: 0.5rem 1rem" disabled>
Add Person
</button>
</form>

<div>
<h2>People (<span id="count">0</span>)</h2>
<ul id="people-list">
<li style="color: #888">No people yet. Add someone above!</li>
</ul>
</div>

<!-- Load the bundled bindings (run `npm run build` first) -->
<script src="dist/bindings.iife.js"></script>

<script>
const HOST = 'ws://localhost:3000';
const DB_NAME = 'browser-ts';

const statusEl = document.getElementById('status');
const nameInput = document.getElementById('name-input');
const addBtn = document.getElementById('add-btn');
const addForm = document.getElementById('add-form');
const peopleList = document.getElementById('people-list');
const countEl = document.getElementById('count');

function renderPeople(conn) {
const people = Array.from(conn.db.person.iter());
countEl.textContent = people.length;
if (people.length === 0) {
peopleList.innerHTML =
'<li style="color: #888;">No people yet. Add someone above!</li>';
return;
}
peopleList.innerHTML = people
.map(p => '<li>' + escapeHtml(p.name || '') + '</li>')
.join('');
}

function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

const conn = DbConnection.builder()
.withUri(HOST)
.withModuleName(DB_NAME)
.withToken(localStorage.getItem('auth_token') || undefined)
.onConnect((conn, identity, token) => {
localStorage.setItem('auth_token', token);
console.log('Connected with identity:', identity.toHexString());

statusEl.textContent = 'Connected';
statusEl.style.color = 'green';
nameInput.disabled = false;
addBtn.disabled = false;

conn.subscriptionBuilder()
.onApplied(() => renderPeople(conn))
.subscribe(['SELECT * FROM person']);

conn.db.person.onInsert(() => renderPeople(conn));
conn.db.person.onDelete(() => renderPeople(conn));
})
.onDisconnect(() => {
console.log('Disconnected from SpacetimeDB');
statusEl.textContent = 'Disconnected';
statusEl.style.color = 'red';
nameInput.disabled = true;
addBtn.disabled = true;
})
.onConnectError((_ctx, error) => {
console.error('Connection error:', error);
statusEl.textContent = 'Error: ' + error.message;
statusEl.style.color = 'red';
})
.build();

addForm.addEventListener('submit', e => {
e.preventDefault();
const name = nameInput.value.trim();
if (name) {
conn.reducers.add({ name });
nameInput.value = '';
}
});
</script>
</body>
</html>
19 changes: 19 additions & 0 deletions templates/browser-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@clockworklabs/browser-ts",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "vite build",
"spacetime:generate": "spacetime generate --lang typescript --out-dir src/module_bindings --project-path spacetimedb",
"spacetime:publish:local": "spacetime publish --project-path spacetimedb --server local",
"spacetime:publish": "spacetime publish --project-path spacetimedb --server maincloud"
},
"dependencies": {
"spacetimedb": "workspace:*"
},
"devDependencies": {
"typescript": "~5.6.2",
"vite": "^7.1.5"
}
}
15 changes: 15 additions & 0 deletions templates/browser-ts/spacetimedb/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "spacetime-module",
"version": "1.0.0",
"description": "",
"scripts": {
"build": "spacetime build",
"publish": "spacetime publish"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"spacetimedb": "1.*"
}
}
33 changes: 33 additions & 0 deletions templates/browser-ts/spacetimedb/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { schema, table, t } from 'spacetimedb/server';

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

spacetimedb.init((_ctx) => {
// Called when the module is initially published
});

spacetimedb.clientConnected((_ctx) => {
// Called every time a new client connects
});

spacetimedb.clientDisconnected((_ctx) => {
// Called every time a client disconnects
});

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!');
});
Loading