Cloud Sync + Auth Workflow

August 8, 2026 · View on GitHub

Learn how to synchronize Praxis state across multiple devices using Praxis Cloud, with an authentication flow that gates access to shared data.

Time: 20–25 minutes
Level: Intermediate
Prerequisites: Getting Started tutorial

What You'll Build

A notes application that:

  • Authenticates users before granting access
  • Syncs notes across clients via Praxis Cloud relay
  • Handles conflicts with CRDT resolution
  • Works offline and reconciles on reconnect

Step 1: Project Setup

mkdir praxis-cloud-notes && cd praxis-cloud-notes
npm init -y
npm install @plures/praxis @plures/pluresdb
npm install -D typescript @types/node

Step 2: Define the Auth Schema

import {
  createApp,
  definePath,
  defineRule,
  defineConstraint,
  RuleResult,
  fact,
} from '@plures/praxis/unified';

// Auth state
const AuthState = definePath<{
  status: 'anonymous' | 'authenticating' | 'authenticated' | 'error';
  userId: string | null;
  token: string | null;
  error: string | null;
}>('auth', {
  status: 'anonymous',
  userId: null,
  token: null,
  error: null,
});

// Application data — gated behind auth
const Notes = definePath<{ id: string; text: string; updatedAt: number }[]>('notes', []);
const SyncStatus = definePath<'idle' | 'syncing' | 'error'>('syncStatus', 'idle');

Step 3: Auth Rules

// Rule — transition auth state machine on login attempt
const processLogin = defineRule({
  id: 'auth.processLogin',
  watch: ['auth'],
  evaluate: (values) => {
    const auth = values['auth'] as { status: string };
    if (auth.status !== 'authenticating') return RuleResult.noop();

    // In production this would call your auth service
    return RuleResult.emit([fact('auth.tokenReceived', { token: 'jwt-token-here' })]);
  },
});

// Rule — enforce session expiry
const sessionExpiry = defineRule({
  id: 'auth.sessionExpiry',
  watch: ['auth'],
  evaluate: (values) => {
    const auth = values['auth'] as { status: string; token: string | null };
    if (auth.status !== 'authenticated' || !auth.token) return RuleResult.noop();

    // Check expiry (simplified — in production decode JWT)
    const expired = false; // placeholder
    if (expired) {
      return RuleResult.emit([fact('auth.sessionExpired', {})]);
    }
    return RuleResult.noop();
  },
});

The expiry branch is intentionally inert in this runnable snippet. A production rule should decode the token expiry, emit auth.sessionExpired, and pair that fact with a rule or mutation that returns auth to the anonymous state.

Step 4: Constraint — Gate Data Access

// Only authenticated users can modify notes
const requireAuth = defineConstraint({
  id: 'notes.requireAuth',
  description: 'User must be authenticated to modify notes',
  watch: ['notes', 'auth'],
  validate: (values) => {
    const auth = values['auth'] as { status: string };
    return auth.status === 'authenticated' || 'Authentication required';
  },
});

Step 5: Cloud Sync with PluresDB

import { connectRelay } from '@plures/praxis/cloud';

// Create the app
const app = createApp({
  name: 'cloud-notes',
  schema: [AuthState, Notes, SyncStatus],
  rules: [processLogin, sessionExpiry],
  constraints: [requireAuth],
});

// Connect to Praxis Cloud relay after authentication
async function connectNotesRelay() {
  const auth = app.query<{ token: string | null }>('auth').current;
  if (!auth.token) throw new Error('Authenticate before connecting sync');
  return connectRelay('https://relay.praxis.plures.dev', {
    appId: 'cloud-notes',
    authToken: auth.token,
    autoSync: true,
  });
}

Step 6: Run the Auth + Sync Flow

// 1. Start anonymous — notes mutation is rejected
const rejected = app.mutate('notes', [{ id: '1', text: 'Hello', updatedAt: Date.now() }]);
console.log(rejected.accepted); // demo only — use OpenTelemetry in production
// Expected output: false

// 2. Trigger login
app.mutate('auth', {
  status: 'authenticating',
  userId: 'user-123',
  token: null,
  error: null,
});

// 3. Simulate the token handler applying the rule-emitted auth token
app.mutate('auth', {
  status: 'authenticated',
  userId: 'user-123',
  token: 'jwt-token-here',
  error: null,
});

console.log(app.query('auth').current.status); // demo only — use OpenTelemetry in production
// Expected output: authenticated

// 4. Connect sync with the authenticated token
const relay = await connectNotesRelay();

// 5. Now notes mutation succeeds
const accepted = app.mutate('notes', [
  { id: '1', text: 'My first synced note', updatedAt: Date.now() },
]);
console.log(accepted.accepted); // demo only — use OpenTelemetry in production
// Expected output: true

// 6. Check sync status
console.log(app.query('syncStatus').current); // demo only — use OpenTelemetry in production
// Expected output: idle

Conflict Resolution

When two clients edit the same note offline, PluresDB uses CRDT-based resolution:

// Client A (offline) edits note 1
app.mutate('notes', [
  { id: '1', text: 'Edited on laptop', updatedAt: 1000 },
]);

// Client B (offline) edits note 1
// (on another device, same room)
// { id: '1', text: 'Edited on phone', updatedAt: 1001 }

// On reconnect, PluresDB resolves via last-write-wins (updatedAt)
// Result: 'Edited on phone' wins because updatedAt is higher

For custom merge strategies, resolve conflicts before sending the next delta:

const localNote = { text: 'Edited on laptop', updatedAt: 1000 };
const remoteNote = { text: 'Edited on phone', updatedAt: 1001 };

const resolveNote = (
  local: { text: string; updatedAt: number },
  remote: { text: string; updatedAt: number },
) => (remote.updatedAt > local.updatedAt ? remote : local);

await relay.sync({
  type: 'delta',
  appId: 'cloud-notes',
  clock: {}, // tutorial placeholder — use your current vector clock in production
  events: [{ tag: 'notes.resolved', payload: resolveNote(localNote, remoteNote) }],
  timestamp: Date.now(),
});

Offline Behavior

Praxis Cloud sync is designed for local-first operation:

ScenarioBehavior
OnlineMutations sync immediately to all connected clients
OfflineMutations are queued locally in PluresDB
ReconnectQueued mutations are replayed and conflicts resolved
Auth expiredSync pauses; re-authenticate to resume

Full Source

Click to expand src/cloud-notes.ts
import {
  createApp,
  definePath,
  defineRule,
  defineConstraint,
  RuleResult,
  fact,
} from '@plures/praxis/unified';
import { connectRelay } from '@plures/praxis/cloud';

const AuthState = definePath<{
  status: 'anonymous' | 'authenticating' | 'authenticated' | 'error';
  userId: string | null;
  token: string | null;
  error: string | null;
}>('auth', { status: 'anonymous', userId: null, token: null, error: null });

const Notes = definePath<{ id: string; text: string; updatedAt: number }[]>('notes', []);
const SyncStatus = definePath<'idle' | 'syncing' | 'error'>('syncStatus', 'idle');

const processLogin = defineRule({
  id: 'auth.processLogin',
  watch: ['auth'],
  evaluate: (values) => {
    const auth = values['auth'] as { status: string };
    if (auth.status !== 'authenticating') return RuleResult.noop();
    return RuleResult.emit([fact('auth.tokenReceived', { token: 'jwt-token-here' })]);
  },
});

const sessionExpiry = defineRule({
  id: 'auth.sessionExpiry',
  watch: ['auth'],
  evaluate: (values) => {
    const auth = values['auth'] as { status: string; token: string | null };
    if (auth.status !== 'authenticated' || !auth.token) return RuleResult.noop();
    const expired = false; // placeholder — decode JWT in production
    if (expired) {
      return RuleResult.emit([fact('auth.sessionExpired', {})]);
    }
    return RuleResult.noop();
  },
});

const requireAuth = defineConstraint({
  id: 'notes.requireAuth',
  description: 'User must be authenticated to modify notes',
  watch: ['notes', 'auth'],
  validate: (values) => {
    const auth = values['auth'] as { status: string };
    return auth.status === 'authenticated' || 'Authentication required';
  },
});

const app = createApp({
  name: 'cloud-notes',
  schema: [AuthState, Notes, SyncStatus],
  rules: [processLogin, sessionExpiry],
  constraints: [requireAuth],
});

async function connectNotesRelay() {
  const auth = app.query<{ token: string | null }>('auth').current;
  if (!auth.token) throw new Error('Authenticate before connecting sync');
  return connectRelay('https://relay.praxis.plures.dev', {
    appId: 'cloud-notes',
    authToken: auth.token,
    autoSync: true,
  });
}

// Rejected — not authenticated
console.log(app.mutate('notes', [{ id: '1', text: 'Hello', updatedAt: Date.now() }]).accepted); // demo only — use OpenTelemetry in production
// false

// Start authentication
app.mutate('auth', { status: 'authenticating', userId: 'user-123', token: null, error: null });

// Authenticate
app.mutate('auth', { status: 'authenticated', userId: 'user-123', token: 'jwt-token-here', error: null });
console.log(app.query('auth').current.status); // authenticated; demo only — use OpenTelemetry in production
const relay = await connectNotesRelay();

// Accepted
console.log(app.mutate('notes', [{ id: '1', text: 'Synced note', updatedAt: Date.now() }]).accepted); // demo only — use OpenTelemetry in production
// true

What's Next