Skip to Content
Jumentix DocsGuidesCreate a SPA or Offline PWA

Creating SPA/PWA with Jumentix

Responsibility in context

  • Owns: Frontend/offline journey for Service Management style apps
  • Used with: designer-core, cana, service-management app hub
  • Not responsible for: Server DB repositories or gRPC Node clients

What it is

This guide describes how to plan and deliver a Single Page Application (SPA) or Progressive Web App (PWA) that stays aligned with backend contracts and supports offline-first storage through Jumentix packages and Service Management designers.

Why it exists

Frontend and backend teams often diverge after the first sprint — DTO shapes drift, realtime channels are guessed, and “offline support” becomes a fragile localStorage hack. Jumentix keeps one design document for domains and contracts, generates matching SDKs, and uses Cana (IndexedDB) for durable offline data.

Prerequisites

ItemRequiredNotes
Bun 1.3.13+YesGetting started
Service Management appRecommendedDomain Designer + Communication Interface Designer
Backend contractsYesOpenAPI and/or AsyncAPI from REST/realtime guides
Browser with IndexedDBYesChrome, Firefox, Safari, Edge

Glossary

TermMeaning on this page
SPASingle-page app — UI loads once; routing happens client-side.
PWAWeb app installable/offline-capable via service worker + local storage.
Domain DesignerService Management tool for entities, relationships, bounded contexts.
Communication Interface DesignerTool picking REST-only vs WebSocket vs gRPC consumption.
designer-core@jumentix/designer-core — validates design documents (domains, relationships).
Cana@jumentix/cana — IndexedDB adapter; not the same as localStorage.
Conflict strategyRule when offline edits sync — e.g. last-write-wins or domain merge.
Service profileEnv keys like JUMENTIX_HTTP_FRAMEWORK chosen in Service Configuration.

Numbered steps

Step 1 — Model business domains first (< 20 minutes)

Open Service Management → Domain Designer and define:

  1. Domains and bounded contexts — what belongs together vs what is separate.
  2. Entities and value objects — fields, types, invariants.
  3. Relationships and constraints — cardinality, required links, uniqueness.
  4. API and event contracts — which resources expose REST vs realtime channels.

This keeps frontend state models and backend OpenAPI/AsyncAPI aligned from iteration one.

Success check: exported design validates with zero blocking issues in designer-core.

Step 2 — Define communication contracts (< 10 minutes)

Use Communication Interface Designer to choose how the SPA talks to backend:

ModeChoose whenClient package
REST onlyCRUD screens, no live push@jumentix/sdk-rest-client
WebSocket + RESTLive dashboards, notifications@jumentix/sdk-websocket-client + REST fallback
gRPC + RESTNode-based BFF consuming backendgRPC SDK (Node) + REST for browser

Contracts become the source for SDK integration — do not hand-write fetch URLs that are not in OpenAPI.

Success check: OpenAPI/AsyncAPI files match designer export; operationIds stable.

Step 3 — Configure service runtime (< 10 minutes)

In Service Management → Service Configuration:

  1. Pick service type: RESTAPI, websocketAPI + RESTAPI, or grpcAPI + RESTAPI.
  2. Set deployment target and runtime profile.
  3. Edit env keys the backend will use:
JUMENTIX_HTTP_FRAMEWORK=express JUMENTIX_REALTIME_API=yes # if using realtime JUMENTIX_REALTIME_API_PROTOCOL=websocket # browser path

Cross-check REST and Realtime guides for startup commands.

Success check: backend dev profile starts with the same env the designer exported.

Step 4 — Build offline-capable PWA storage (< 30 minutes)

For offline-first architecture:

  1. Validate design with @jumentix/designer-core before persisting data.
  2. Open Cana IndexedDB database matching your entity stores.
  3. Read/write locally during offline periods; queue sync operations.
  4. Sync asynchronously when online — use REST/WS contracts, not ad-hoc JSON.
  5. Document conflict strategy explicitly (last-write-wins or domain-specific merge).

Static offline write:

const db = await cana.open({ name: 'my-pwa', version: 1 });
await db.put('tasks', { id: '1', title: 'Draft offline', updatedAt: Date.now() });

Success check: data survives browser reload; client.backend reports indexeddb.

Step 5 — Validate delivery readiness (< 15 minutes)

Run these gates before calling the PWA “done”:

GateCommand / action
Contract alignmentOpenAPI/AsyncAPI diff vs designer export
Backend CIbun run test:unit, bun run oas:check-routes
Frontend buildProduction bundle builds without contract import errors
Offline smokeLoad app offline; Cana records still readable
Playgroundsdesigner-core + Cana Run green below

Success check: all gates pass; offline reload shows persisted entities.

Step 6 — Wire designers to storage (hands-on)

Validate a design document, then persist records with Cana.

designer-core — validates { domains, relationships } shape:

Validate a design

Normalize the sample model and collect validation issues (real designer-core API).

const raw = api.buildSampleModelPayload();
const state = api.normalizeStatePayload(raw);
const issues = api.collectModelIssues(state);
const errors = issues.filter((issue) => issue.severity === 'error');
return {
  ok: errors.length === 0,
  issueCount: issues.length,
  errorCount: errors.length,
  sample: issues.slice(0, 3)
};

Cana — IndexedDB put/get inside validated entity stores:

Getting started

Open a client, create Category and Task records, then read them back.

const client = cana.createClient({
  name: dbName,
  schema: {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byCompleted', keyPath: 'completed' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
}
});
await client.open();
await client.table('categories').add({
  id: 'work',
  name: 'Work',
  color: '#2563eb',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
await client.table('tasks').add({
  id: 'task-1',
  title: 'Write the Cana tutorial',
  categoryId: 'work',
  completed: false,
  priority: 'high',
  createdAt: Date.now(),
  updatedAt: Date.now()
});
return {
  backend: client.backend,
  category: await client.table('categories').get('work'),
  task: await client.table('tasks').get('task-1')
};

Common errors

SymptomLikely causeFixVerify success
designer-core Run throws on forEachInvalid payload shape (entities toy object)Use { domains, relationships } via buildSampleModelPayload patternPlayground { ok: true }
TransactionInactive in CanaNetwork I/O inside Cana transactionOnly IndexedDB awaits inside tx callbackCana playground Run green
SPA calls wrong URLHand-written paths bypass OpenAPIGenerate calls from @jumentix/sdk-rest-clientNetwork tab matches spec paths
Offline data missing after reloadUsed localStorage instead of CanaMigrate stores to Cana IndexedDBReload retains records
Realtime works locally, fails prodWS URL not in AsyncAPI / env mismatchAlign Service Configuration env with deploymentWS connect + subscribe smoke
Sync conflicts silently overwriteNo documented conflict strategyPick LWW or merge rules per entityTest offline edit + online sync

Junior checklist (“I can …”)

  • Model one bounded context in Domain Designer with entities and relationships.
  • Choose REST-only or WebSocket+REST in Communication Interface Designer.
  • Explain why Cana is not localStorage.
  • Run designer-core playground (Run green, valid model issues list).
  • Run Cana playground (Run green, records created).
  • Name my conflict strategy for at least one entity.
  • Link frontend SDK calls to OpenAPI operationIds.

Next step

Deepen offline and design skills on Cana usage and designer-core usage, then explore the full Packages map for persistence and communication helpers.