Examples
Firebase
Keep Firebase for what it does well, signing users in, and keep the synced data in a database next to your own backend. Ferry runs in that backend: the app reads and writes its local database, and syncs through your route for the users Firebase Auth signed in.
| Piece | Here |
|---|---|
| Who the user is | Firebase Auth: its ID token, verified |
| Where the server keeps data | Your Postgres or SQLite, through Ferry's stores |
| Where the server runs | Your backend: any of the backends |
| Where the device keeps data | IndexedDB |
It uses the schema of the Quick start.
The server
auth verifies the ID token the app sends, with the Admin SDK, and answers its user's uid:
import { initializeApp } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import { createSyncServer, nodeSqlite, sqliteStore } from 'ferrysync/server';
import { DatabaseSync } from 'node:sqlite';
import { schema } from './schema';
initializeApp();
/** The Firebase user the request's ID token belongs to, or null. */
const userOf = async (request: Request) => {
const token = request.headers.get('authorization')?.replace(/^Bearer /, '');
if (!token) {
return null;
}
try {
return (await getAuth().verifyIdToken(token)).uid;
} catch {
// Expired or forged: a 401. The next request calls `headers` again, with a fresh token.
return null;
}
};
export const syncServer = createSyncServer({
schema,
store: sqliteStore(nodeSqlite(new DatabaseSync('sync.db'))),
auth: userOf,
claimSpace: ({ table, space, row }) => table === 'projects' && space === `project:${row.id}`,
});The client
The app starts on the device alone. Once Firebase has restored its session, the client picks your route or the device; at sign-in it connects and uploads what the user did before. See Accounts.
import { initializeApp } from 'firebase/app';
import { GoogleAuthProvider, getAuth, signInWithPopup, signOut as leave } from 'firebase/auth';
import { createSync, http, indexedDbStorage, local } from 'ferrysync';
import { schema } from './schema';
const auth = getAuth(initializeApp({ apiKey: '…', authDomain: '…', projectId: '…' }));
/** Your route, with a current ID token on every request: Firebase refreshes it as needed. */
const backend = http({
url: '/api/sync',
headers: async () => ({
authorization: `Bearer ${(await auth.currentUser?.getIdToken()) ?? ''}`,
}),
});
await auth.authStateReady();
export const sync = createSync({
schema,
storage: indexedDbStorage('tasks'),
backend: auth.currentUser ? backend : local(),
});
export const signIn = async () => {
await signInWithPopup(auth, new GoogleAuthProvider());
await sync.connect(backend, { upload: true });
};
export const signOut = async () => {
await leave(auth);
await sync.reset(local());
};Gotchas
- Firestore is not the store here. Ferry's server keeps an ordered log next to the rows, in SQL.
Keep Firestore for what you already have there, and the synced tables in Postgres or SQLite. A
store over another database is possible: it is a
ServerStore, proven byserverStoreConformanceCasesfromferrysync/testing. - Wait for
authStateReady()before choosing the backend. Until thencurrentUserisnulleven for a signed-in user, and the app would start on the device alone. - Connect from the sign-in, not from
onAuthStateChanged. That fires on every restore, andconnectwithupload: truemust run once per sign-in.