Skip to content

Commit f4b3bb7

Browse files
Merge pull request #6 from sqliteai/sport-tracker-app-example
feat(example): sport tracker app initial commit
2 parents a8aa6b2 + 1968c53 commit f4b3bb7

28 files changed

+3491
-3
lines changed

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
*.xcbkptlist
55
*.plist
66
/build
7-
/dist
7+
**/dist/**
88
/coverage
99
*.sqlite
1010
*.a
1111
unittest
1212
/curl/src
13-
.vscode
13+
.vscode
14+
**/node_modules/**
15+
.env
16+
package-lock.json

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ SELECT cloudsync_terminate();
237237

238238
### For a Complete Example
239239

240-
See the [Simple Todo Database example](./examples/simple-todo-db/) for a comprehensive walkthrough including:
240+
See the [examples](./examples/simple-todo-db/) directory for a comprehensive walkthrough including:
241241
- Multi-device collaboration
242242
- Offline scenarios
243243
- Row-level security setup
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copy from from the SQLite Cloud Dashboard
2+
# eg: sqlitecloud://myhost.cloud:8860/my-remote-database.sqlite
3+
VITE_SQLITECLOUD_CONNECTION_STRING=
4+
# The database name
5+
# eg: my-remote-database.sqlite
6+
VITE_SQLITECLOUD_DATABASE=
7+
# Your SQLite Cloud API key
8+
# Copy it from the SQLite Cloud Dashboard -> Settings -> API Keys
9+
VITE_SQLITECLOUD_API_KEY=
10+
# Your SQLite Cloud url for APIs
11+
# Get it from the SQLite Cloud Dashboard in the Weblite section
12+
# eg: https://myhost.cloud
13+
VITE_SQLITECLOUD_API_URL=
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
# Sport Tracker app with SQLite Sync 🚵
2+
3+
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).
4+
5+
6+
## Features
7+
8+
From a **user experience** perspective, this is a simple sport tracking application where users can:
9+
- Create accounts and log activities (running, cycling, swimming, etc.)
10+
- View personal statistics and workout history
11+
- Access "Coach Mode" for managing multiple users' workouts
12+
13+
From a **developer perspective**, this app showcases:
14+
- **Offline-first** architecture with sync to the remote database using **SQLite Sync** extension for SQLite
15+
- **Row-Level Security (RLS)** implementation for data isolation and access control on the SQLite Cloud database
16+
- **Access Tokens** for secure user authentication with SQLite Sync and RLS policy enforcement
17+
- **Multi-user** data isolation and sharing patterns across different user sessions
18+
19+
## Setup Instructions
20+
21+
### 1. Prerequisites
22+
- Node.js 18+
23+
- [SQLite Cloud account](https://sqlitecloud.io)
24+
25+
### 2. Database Setup
26+
1. Create database in [SQLite Cloud Dashboard](https://dashboard.sqlitecloud.io/).
27+
2. Execute the exact schema from `sport-tracker-schema.sql`.
28+
3. Enable OffSync for all tables on the remote database from the **SQLite Cloud Dashboard -> Databases**.
29+
4. Enable and configure RLS policies on the **SQLite Cloud Dashboard -> Databases**. See the file `rls-policies.md`.
30+
31+
### 3. Environment Configuration
32+
33+
Rename the `.env.example` into `.env` and fill with your values.
34+
35+
### 4. Installation & Run
36+
37+
```bash
38+
npm install
39+
npm run dev
40+
```
41+
42+
> This app uses the packed WASM version of SQLite with the [SQLite Sync extension enabled](https://www.npmjs.com/package/@sqliteai/sqlite-sync-wasm).
43+
44+
## Demo Use Case: Multi-User Sync Scenario
45+
46+
This walkthrough demonstrates how SQLite Sync handles offline-first synchronization between multiple users:
47+
48+
### The Story: Bob the Runner & Coach Sarah
49+
50+
1. **Bob starts tracking offline** 📱
51+
- Open [localhost:5173](http://localhost:5173) in your browser
52+
- Create user `bob` and add some activities
53+
- Notice Bob's data is stored locally - no internet required!
54+
55+
2. **Bob goes online and syncs** 🌐
56+
- Click `SQLite Sync` to authenticate SQLite Sync
57+
- Click `Sync & Refresh` - this generates an Access Token and synchronizes Bob's local data to the cloud
58+
- Bob's activities are now replicated in the cloud
59+
60+
3. **Coach Sarah joins from another device** 👩‍💼
61+
- Open a new private/incognito browser window at [localhost:5173](http://localhost:5173)
62+
- Create user `coach` (this triggers special coach privileges via RLS)
63+
- Enable `SQLite Sync` and click `Sync & Refresh`. Coach can now see Bob's synced activities thanks to RLS policies
64+
65+
4. **Coach creates a workout for Bob** 💪
66+
- Coach creates a workout assigned to Bob
67+
- Click `Sync & Refresh` to upload the workout to the cloud
68+
69+
5. **Bob receives his workout** 📲
70+
- Go back to Bob's browser window
71+
- Click `Sync & Refresh` - Bob's local database downloads the new workout from Coach
72+
- Bob can now see his personalized workout
73+
74+
6. **Bob gets a new device** 📱➡️💻
75+
- Log out Bob, then select it and click `Restore from cloud`
76+
- This simulates Bob logging in from a completely new device with no local data
77+
- Enable `SQLite Sync` and sync - all of Bob's activities and workouts are restored from the cloud
78+
79+
**Key takeaway**: Users can work offline, sync when convenient, and seamlessly restore data on new devices!
80+
81+
82+
## SQLite Sync Implementation
83+
84+
### 1. Database Initialization
85+
86+
```typescript
87+
// database.ts - Initialize sync for each table
88+
export class Database {
89+
async initSync() {
90+
await this.exec('SELECT cloudsync_init("users")');
91+
await this.exec('SELECT cloudsync_init("activities")');
92+
await this.exec('SELECT cloudsync_init("workouts")');
93+
}
94+
}
95+
```
96+
97+
### 2. Token Management
98+
99+
```typescript
100+
// SQLiteSync.ts - Access token handling
101+
private async getValidToken(userId: string, name: string): Promise<string> {
102+
const storedTokenData = localStorage.getItem('token');
103+
104+
if (storedTokenData) {
105+
const parsed: TokenData = JSON.parse(storedTokenData);
106+
const tokenExpiry = new Date(parsed.expiresAt);
107+
108+
if (tokenExpiry > new Date()) {
109+
return parsed.token; // Use cached token
110+
}
111+
}
112+
113+
// Fetch new token from API
114+
const tokenData = await this.fetchNewToken(userId, name);
115+
localStorage.setItem('token', JSON.stringify(tokenData));
116+
return tokenData.token;
117+
}
118+
```
119+
120+
Then authorize SQLite Sync with the token. This operation is executed again when tokens expire and a new one is provided.
121+
122+
```typescript
123+
async sqliteSyncSetToken(token: string) {
124+
await this.exec(`SELECT cloudsync_network_set_token('${token}')`);
125+
}
126+
```
127+
128+
### 3. Synchronization
129+
130+
The sync operation sends local changes to the cloud and receives remote changes:
131+
132+
```typescript
133+
async sqliteSyncNetworkSync() {
134+
await this.exec('SELECT cloudsync_network_sync()');
135+
}
136+
```
137+
138+
## Row-Level Security (RLS)
139+
140+
This app demonstrates **Row-Level Security** configured in the SQLite Cloud Dashboard. RLS policies ensure:
141+
142+
- **Users** can only see their own activities and workouts
143+
- **Coaches** can access all users' data and create workouts for the users
144+
- **Data isolation** is enforced at the database level
145+
146+
### Example RLS Policies
147+
148+
```sql
149+
-- Policy for selecting activities
150+
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
151+
152+
-- Policy for inserting into workouts table
153+
json_extract(auth_json(), '$.name') = 'coach'
154+
```
155+
156+
> **Note**: Configure RLS policies in your SQLite Cloud Dashboard under Databases → RLS
157+
158+
## Security Considerations
159+
160+
⚠️ **Important**: This demo includes client-side API key usage for simplicity. In production:
161+
162+
- Never expose API keys in client code
163+
- Use **server-side generation** for Access Tokens
164+
- Implement a proper authentication flow
165+
166+
## Documentation Links
167+
168+
Explore the code and learn more:
169+
170+
- **SQLite Sync API**: [sqlite-sync](https://github.com/sqliteai/sqlite-sync/blob/main/API.md)
171+
- **Access Tokens Guide**: [SQLite Cloud Access Tokens](https://docs.sqlitecloud.io/docs/access-tokens)
172+
- **Row-Level Security**: [SQLite Cloud RLS](https://docs.sqlitecloud.io/docs/rls)
173+
174+
## Performance considerations
175+
176+
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)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Sport Tracker</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "sport-tracking-app",
3+
"private": true,
4+
"version": "0.0.1",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"devDependencies": {
12+
"typescript": "~5.8.3",
13+
"vite": "^7.0.0"
14+
},
15+
"dependencies": {
16+
"@sqliteai/sqlite-sync-wasm": "^3.49.2-sync-0.8.9",
17+
"@types/react": "^19.1.8",
18+
"@types/react-dom": "^19.1.6",
19+
"@vitejs/plugin-react": "^4.6.0",
20+
"react": "^19.1.0",
21+
"react-dom": "^19.1.0"
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
## RLS Policies
2+
3+
4+
### Users
5+
6+
#### SELECT
7+
8+
```sql
9+
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
10+
```
11+
12+
#### INSERT
13+
14+
```sql
15+
auth_userid() = NEW.id
16+
```
17+
18+
#### UPDATE
19+
20+
_No policy_
21+
22+
#### DELETE
23+
24+
_No policy_
25+
26+
---
27+
28+
### Activities
29+
30+
#### SELECT
31+
32+
```sql
33+
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
34+
```
35+
36+
#### INSERT
37+
38+
```sql
39+
auth_userid() = NEW.user_id
40+
```
41+
42+
#### UPDATE
43+
44+
_No policy_
45+
46+
#### DELETE
47+
48+
```sql
49+
auth_userid() = OLD.user_id OR json_extract(auth_json(), '$.name') = 'coach'
50+
```
51+
52+
---
53+
54+
### Workouts
55+
56+
#### SELECT
57+
58+
```sql
59+
auth_userid() = user_id OR json_extract(auth_json(), '$.name') = 'coach'
60+
```
61+
62+
#### INSERT
63+
64+
```sql
65+
json_extract(auth_json(), '$.name') = 'coach'
66+
```
67+
68+
#### UPDATE
69+
70+
```sql
71+
OLD.user_id = auth_userid() OR json_extract(auth_json(), '$.name') = 'coach'
72+
```
73+
74+
#### DELETE
75+
76+
```sql
77+
json_extract(auth_json(), '$.name') = 'coach'
78+
```
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-- SQL schema
2+
-- Use this exact schema to create the remote database on the on SQLite Cloud
3+
4+
CREATE TABLE IF NOT EXISTS users (
5+
id TEXT PRIMARY KEY NOT NULL, -- UUID's HIGHLY RECOMMENDED for global uniqueness
6+
name TEXT UNIQUE NOT NULL DEFAULT ''
7+
);
8+
9+
CREATE TABLE IF NOT EXISTS activities (
10+
id TEXT PRIMARY KEY NOT NULL, -- UUID's HIGHLY RECOMMENDED for global uniqueness
11+
type TEXT NOT NULL DEFAULT 'runnning',
12+
duration INTEGER,
13+
distance REAL,
14+
calories INTEGER,
15+
date TEXT,
16+
notes TEXT,
17+
user_id TEXT,
18+
FOREIGN KEY (user_id) REFERENCES users (id)
19+
);
20+
21+
CREATE TABLE IF NOT EXISTS workouts (
22+
id TEXT PRIMARY KEY NOT NULL, -- UUID's HIGHLY RECOMMENDED for global uniqueness
23+
name TEXT,
24+
type TEXT,
25+
duration INTEGER,
26+
exercises TEXT,
27+
date TEXT,
28+
completed INTEGER DEFAULT 0,
29+
user_id TEXT
30+
);

0 commit comments

Comments
 (0)