Skip to content
Merged
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: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
*.xcbkptlist
*.plist
/build
/dist
**/dist/**
/coverage
*.sqlite
*.a
unittest
/curl/src
.vscode
.vscode
**/node_modules/**
.env
package-lock.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ SELECT cloudsync_terminate();

### For a Complete Example

See the [Simple Todo Database example](./examples/simple-todo-db/) for a comprehensive walkthrough including:
See the [examples](./examples/simple-todo-db/) directory for a comprehensive walkthrough including:
- Multi-device collaboration
- Offline scenarios
- Row-level security setup
Expand Down
13 changes: 13 additions & 0 deletions examples/sport-tracker-app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copy from from the SQLite Cloud Dashboard
# eg: sqlitecloud://myhost.cloud:8860/my-remote-database.sqlite
VITE_SQLITECLOUD_CONNECTION_STRING=
# The database name
# eg: my-remote-database.sqlite
VITE_SQLITECLOUD_DATABASE=
# Your SQLite Cloud API key
# Copy it from the SQLite Cloud Dashboard -> Settings -> API Keys
VITE_SQLITECLOUD_API_KEY=
# Your SQLite Cloud url for APIs
# Get it from the SQLite Cloud Dashboard in the Weblite section
# eg: https://myhost.cloud
VITE_SQLITECLOUD_API_URL=
176 changes: 176 additions & 0 deletions examples/sport-tracker-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Sport Tracker app with SQLite Sync 🚵

A Vite/React demonstration app showcasing [**SQLite Sync**](https://github.com/sqliteai/sqlite-sync) implementation for **offline-first** data synchronization across multiple devices. This example illustrates how to integrate SQLite AI's sync capabilities into modern web applications with proper authentication via [Access Token](https://docs.sqlitecloud.io/docs/access-tokens) and [Row-Level Security (RLS)](https://docs.sqlitecloud.io/docs/rls).


## Features

From a **user experience** perspective, this is a simple sport tracking application where users can:
- Create accounts and log activities (running, cycling, swimming, etc.)
- View personal statistics and workout history
- Access "Coach Mode" for managing multiple users' workouts

From a **developer perspective**, this app showcases:
- **Offline-first** architecture with sync to the remote database using **SQLite Sync** extension for SQLite
- **Row-Level Security (RLS)** implementation for data isolation and access control on the SQLite Cloud database
- **Access Tokens** for secure user authentication with SQLite Sync and RLS policy enforcement
- **Multi-user** data isolation and sharing patterns across different user sessions

## Setup Instructions

### 1. Prerequisites
- Node.js 18+
- [SQLite Cloud account](https://sqlitecloud.io)

### 2. Database Setup
1. Create database in [SQLite Cloud Dashboard](https://dashboard.sqlitecloud.io/).
2. Execute the exact schema from `sport-tracker-schema.sql`.
3. Enable OffSync for all tables on the remote database from the **SQLite Cloud Dashboard -> Databases**.
4. Enable and configure RLS policies on the **SQLite Cloud Dashboard -> Databases**. See the file `rls-policies.md`.

### 3. Environment Configuration

Rename the `.env.example` into `.env` and fill with your values.

### 4. Installation & Run

```bash
npm install
npm run dev
```

> This app uses the packed WASM version of SQLite with the [SQLite Sync extension enabled](https://www.npmjs.com/package/@sqliteai/sqlite-sync-wasm).

## Demo Use Case: Multi-User Sync Scenario

This walkthrough demonstrates how SQLite Sync handles offline-first synchronization between multiple users:

### The Story: Bob the Runner & Coach Sarah

1. **Bob starts tracking offline** 📱
- Open [localhost:5173](http://localhost:5173) in your browser
- Create user `bob` and add some activities
- Notice Bob's data is stored locally - no internet required!

2. **Bob goes online and syncs** 🌐
- Click `SQLite Sync` to authenticate SQLite Sync
- Click `Sync & Refresh` - this generates an Access Token and synchronizes Bob's local data to the cloud
- Bob's activities are now replicated in the cloud

3. **Coach Sarah joins from another device** 👩‍💼
- Open a new private/incognito browser window at [localhost:5173](http://localhost:5173)
- Create user `coach` (this triggers special coach privileges via RLS)
- Enable `SQLite Sync` and click `Sync & Refresh`. Coach can now see Bob's synced activities thanks to RLS policies

4. **Coach creates a workout for Bob** 💪
- Coach creates a workout assigned to Bob
- Click `Sync & Refresh` to upload the workout to the cloud

5. **Bob receives his workout** 📲
- Go back to Bob's browser window
- Click `Sync & Refresh` - Bob's local database downloads the new workout from Coach
- Bob can now see his personalized workout

6. **Bob gets a new device** 📱➡️💻
- Log out Bob, then select it and click `Restore from cloud`
- This simulates Bob logging in from a completely new device with no local data
- Enable `SQLite Sync` and sync - all of Bob's activities and workouts are restored from the cloud

**Key takeaway**: Users can work offline, sync when convenient, and seamlessly restore data on new devices!


## SQLite Sync Implementation

### 1. Database Initialization

```typescript
// database.ts - Initialize sync for each table
export class Database {
async initSync() {
await this.exec('SELECT cloudsync_init("users")');
await this.exec('SELECT cloudsync_init("activities")');
await this.exec('SELECT cloudsync_init("workouts")');
}
}
```

### 2. Token Management

```typescript
// SQLiteSync.ts - Access token handling
private async getValidToken(userId: string, name: string): Promise<string> {
const storedTokenData = localStorage.getItem('token');

if (storedTokenData) {
const parsed: TokenData = JSON.parse(storedTokenData);
const tokenExpiry = new Date(parsed.expiresAt);

if (tokenExpiry > new Date()) {
return parsed.token; // Use cached token
}
}

// Fetch new token from API
const tokenData = await this.fetchNewToken(userId, name);
localStorage.setItem('token', JSON.stringify(tokenData));
return tokenData.token;
}
```

Then authorize SQLite Sync with the token. This operation is executed again when tokens expire and a new one is provided.

```typescript
async sqliteSyncSetToken(token: string) {
await this.exec(`SELECT cloudsync_network_set_token('${token}')`);
}
```

### 3. Synchronization

The sync operation sends local changes to the cloud and receives remote changes:

```typescript
async sqliteSyncNetworkSync() {
await this.exec('SELECT cloudsync_network_sync()');
}
```

## Row-Level Security (RLS)

This app demonstrates **Row-Level Security** configured in the SQLite Cloud Dashboard. RLS policies ensure:

- **Users** can only see their own activities and workouts
- **Coaches** can access all users' data and create workouts for the users
- **Data isolation** is enforced at the database level

### Example RLS Policies

```sql
-- Policy for selecting activities
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'

-- Policy for inserting into workouts table
json_extract(auth_json(), '$.name') = 'coach'
```

> **Note**: Configure RLS policies in your SQLite Cloud Dashboard under Databases → RLS

## Security Considerations

⚠️ **Important**: This demo includes client-side API key usage for simplicity. In production:

- Never expose API keys in client code
- Use **server-side generation** for Access Tokens
- Implement a proper authentication flow

## Documentation Links

Explore the code and learn more:

- **SQLite Sync API**: [sqlite-sync](https://github.com/sqliteai/sqlite-sync/blob/main/API.md)
- **Access Tokens Guide**: [SQLite Cloud Access Tokens](https://docs.sqlitecloud.io/docs/access-tokens)
- **Row-Level Security**: [SQLite Cloud RLS](https://docs.sqlitecloud.io/docs/rls)

## Performance considerations

The database is persisted in the Origin-Private FileSystem OPFS (if available) but performance is much lower. Read more [here](https://sqlite.org/wasm/doc/trunk/persistence.md)
13 changes: 13 additions & 0 deletions examples/sport-tracker-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sport Tracker</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
23 changes: 23 additions & 0 deletions examples/sport-tracker-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "sport-tracking-app",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.8.3",
"vite": "^7.0.0"
},
"dependencies": {
"@sqliteai/sqlite-sync-wasm": "^3.49.2-sync-0.8.9",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}
1 change: 1 addition & 0 deletions examples/sport-tracker-app/public/vite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
78 changes: 78 additions & 0 deletions examples/sport-tracker-app/rls-policies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
## RLS Policies


### Users

#### SELECT

```sql
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
```

#### INSERT

```sql
auth_userid() = NEW.id
```

#### UPDATE

_No policy_

#### DELETE

_No policy_

---

### Activities

#### SELECT

```sql
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
```

#### INSERT

```sql
auth_userid() = NEW.user_id
```

#### UPDATE

_No policy_

#### DELETE

```sql
auth_userid() = OLD.user_id OR json_extract(auth_json(), '$.name') = 'coach'
```

---

### Workouts

#### SELECT

```sql
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
```

#### INSERT

```sql
json_extract(auth_json(), '$.name') = 'coach'
```

#### UPDATE

```sql
OLD.user_id = auth_userid() OR json_extract(auth_json(), '$.name') = 'coach'
```

#### DELETE

```sql
json_extract(auth_json(), '$.name') = 'coach'
```
30 changes: 30 additions & 0 deletions examples/sport-tracker-app/sport-tracker-schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- SQL schema
-- Use this exact schema to create the remote database on the on SQLite Cloud

CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL, -- UUID's HIGHLY RECOMMENDED for global uniqueness
name TEXT UNIQUE NOT NULL DEFAULT ''
);

CREATE TABLE IF NOT EXISTS activities (
id TEXT PRIMARY KEY NOT NULL, -- UUID's HIGHLY RECOMMENDED for global uniqueness
type TEXT NOT NULL DEFAULT 'runnning',
duration INTEGER,
distance REAL,
calories INTEGER,
date TEXT,
notes TEXT,
user_id TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
);

CREATE TABLE IF NOT EXISTS workouts (
id TEXT PRIMARY KEY NOT NULL, -- UUID's HIGHLY RECOMMENDED for global uniqueness
name TEXT,
type TEXT,
duration INTEGER,
exercises TEXT,
date TEXT,
completed INTEGER DEFAULT 0,
user_id TEXT
);
Loading
Loading