Skip to content

Frontend Overview

The SchemaStack frontend lives in the ../schemastack-fe repository. It's an Angular 20 multi-project workspace.

Tech Stack

LayerTechnology
FrameworkAngular 20.3.6 (standalone components)
State managementNgRx 20.0.1 (effects, entity, store-devtools)
UI frameworkAngular Material 20.2.9 (Material Design 3)
StylingSCSS + Tailwind CSS 3.4.18 + custom utility classes
Unit testingKarma + Jasmine
E2E testingPlaywright 1.56.1
BuildAngular CLI with application builder

Project Structure

The workspace contains three projects:

schemastack-fe/
├── projects/
│   ├── admin/           ← Admin Console application
│   │   └── src/
│   │       ├── app/     (auth, organization, workspace, members, settings)
│   │       └── styles/  (_utilities.scss — MD3 utility classes)
│   ├── spread/          ← Data Platform application
│   │   └── src/
│   │       ├── app/     (views, columns, data grid, presets, filters)
│   │       └── styles/  (_utilities.scss — same utility classes)
│   └── shared/          ← Shared Angular library
│       └── src/         (shared components, services, models)
├── e2e/                 ← Playwright E2E tests (11 spec files)
└── angular.json         ← Workspace configuration

Admin

The Admin Console — organisation management, workspace setup, member/role management, security settings (2FA, sessions), personal profile.

Spread

The Data Platform — spreadsheet-style interface for viewing and editing data. Handles views, columns, real-time collaboration (SSE), presets, filters, bulk operations.

Shared (@schemastack/shared)

Local Angular library used by both apps. Exports:

CategoryExports
AuthAuthService, TokenService, LoginDialogComponent, auth interceptor, auth guard
NotificationNotificationService, NotificationSnackbarComponent
LoadingLoadingBarService, loading bar component + interceptor
DialogsConfirmActionDialogComponent
ServicesNetworkStatusService
UtilitiesextractErrorMessage(), extractValidationErrors()

TokenService stores JWT in localStorage (key: 'token') and exposes reactive observables: token$ (emits on every change) and tokenAvailable$ (emits only when a valid token becomes available). Guards against SSR via typeof window checks.

Commands

bash
ng serve              # Start admin (default) on localhost:4200
ng serve spread       # Start spread on localhost:4200
ng build admin        # Production build (admin)
ng build spread       # Production build (spread)
ng test               # Run unit tests (Karma/Jasmine)
npm run test:brave     # Run tests in Brave browser
npm run test:brave:ci  # Run tests in CI mode
npm run e2e           # Run Playwright E2E tests (headless)
npm run e2e:headed    # Run Playwright E2E tests (visible browser)

State Management

Both apps use NgRx with the standard pattern: actions → reducers (state transitions) → selectors (derived state) → effects (side effects, API calls, SSE subscriptions).

Admin Store

typescript
interface AppState {
  auth: AuthState;             // JWT, user, org selection, 2FA state
  organization: OrganizationState;
  loading: LoadingState;
  workspace: WorkspaceState;
}
SliceKey StateEffects
authuser, token, selectedOrganisation, availableOrganisations, needsOrgSelection, requires2FA, tempTokenLogin, logout, org selection, 2FA verification
organizationOrganisation CRUD stateOrganisation management
workspaceWorkspace CRUD stateWorkspace CRUD + workspace-sse.effects.ts for SSE events
loadingGlobal loading indicators

Spread Store

typescript
interface AppState {
  view: ViewState;       // The core — views, columns, data, presets, selection, bulk ops
  activity: ActivityState; // Activity feed from SSE events
  auth: AuthState;       // Current user and login form
  ui: UIState;           // Inline editing state (view/column name editing)
}

The view slice is the most complex state in the application:

Sub-statePurpose
View metadatacurrentView, allViews, viewNotFound, workspaceNotFound
ColumnsbaseColumns (from backend, for reset), columns (current), displayedColumns (ordered keys)
DataviewData (row records), totalRecords, pageSize (100), loadedPages (virtual scrolling)
Sort/FiltersortRules, filterRules, filterPanelOpen
SelectionThree modes (none, individual, all), uses selectedRowIds/excludedRowIds for efficient million-row selection
Bulk operationsJob tracking: jobId, operationType, status, progress, errors (max 100), downloads
PresetsallPresets, activePreset, hasUnsavedChanges, dialog states
Pending rowsSSE batching queue (see adaptive batching below)
Add rowForm schema, validation errors
ConstraintsColumn + entity constraints
Column locksTracks schema-altering operations in progress (ALTER TABLE)
Properties panelStack-based navigation with modes (default, edit-column, edit-view, add-relationship-column)
Optimistic updatespendingOperations for view/column create/update/delete

Spread View Effects (13 classes)

EffectHandles
ViewLoadEffectsLoading view data, pagination
ViewCrudEffectsView create, update, delete
ColumnCrudEffectsColumn create, update, delete, reorder. Presentation-only changes (displayName, hidden, width, position) stay FE-only
ColumnSchemaEffectsSchema changes (handles 200 sync vs 202 async responses)
ColumnGroupingEffectsColumn display groups
FilterSortEffectsFilter and sort — triggers data reload on change
SelectionBulkEffectsRow selection, bulk delete/update/export
SSERoutingEffectsRoutes SSE events to NgRx actions, includes adaptive batching
PresetEffectsFilter preset CRUD
CellEditEffectsSingle cell, bulk cell, batch cell edits
RowCrudEffectsAdd row dialog, row creation
RelationshipEffectsRelationship picker, add related column
ConstraintEffectsColumn and entity constraint CRUD

Real-Time (SSE)

The two apps use different SSE implementations — this is important for debugging and modification:

Admin: Native EventSource

OrganizationMemberEventsService uses the browser's native EventSource API:

  • Connects to /sse/member-events?token={jwt} (JWT as query param — EventSource cannot send custom headers)
  • Event types: connected, member-create, member-update, member-delete
  • Reconnection: exponential backoff (base 1s, max 5 attempts, formula delay * 2^(attempts-1))

Spread: fetch + ReadableStream

WorkspaceEventService uses fetch() + ReadableStream instead of EventSource:

  • Connects to /sse/workspace/{workspaceId}/stream
  • Custom headers: Authorization: Bearer {token}, Accept: text/event-stream, X-Client-Id: {clientId}
  • Manual SSE parsing: reads Uint8Array chunks, buffers text, splits on \n\n, extracts data: lines
  • Three filtered streams: getEvents() (all), getWorkspaceEvents() (view/bulk/workspace), getViewDataEvents(viewId) (row/column/cell for a specific view)
  • Reconnection: exponential backoff via AppConfigService (configurable max attempts and delay)
  • Auto-reconnects when token becomes available (subscribes to tokenService.tokenAvailable$)

Client ID Echo Prevention

Each browser tab generates a crypto.randomUUID() via ClientIdService. The clientIdInterceptor attaches it as X-Client-Id on every HTTP request. The backend echoes it as originClientId in SSE events.

This is distinct from userId filtering — it prevents echo within the same user's multiple tabs. ClientIdService.isOwnEvent(originClientId) returns true if the event came from this specific tab.

Adaptive Row Batching

When remote users insert rows at high frequency, the spread app switches from immediate DOM insertion to a queued mode:

typescript
const BATCH_CONFIG = {
  WINDOW_MS: 1000,           // Rolling time window
  AUTO_INSERT_THRESHOLD: 3,  // Max inserts per window before batch mode
  SHOW_PILL_THRESHOLD: 1,    // Queued rows before showing pill
};
  • If > 3 inserts arrive within 1 second → batch mode activates
  • New rows are queued into PendingRowsState.rows instead of inserted into the data grid
  • A "N new rows" pill appears; user clicks to flush all pending rows at once
  • Prevents UI jank during bulk imports by other users

Routing & Guards

Admin Routes

PathComponentGuard
/dashboardDashboardOrganizationGuard
/organization/**Lazy-loaded org routesOrganizationGuard
/settings/personal|security|notificationsSettings pagesauthGuard
/workspaces/**Lazy-loaded workspace routesOrganizationGuard
/:workspaceSlug/workspace/**Workspace detailOrganizationGuard
/auth/**Login, register, verifyUnauthenticated layout
/account-recoveryAccount recovery
/confirm-deletion/:tokenConfirm account deletion

Spread Routes

PathComponentGuard
/loginLoginloginGuard (redirects authenticated users)
/:orgSlug/:workspaceSlug/:viewSlugViewauthGuard
/:orgSlug/:workspaceSlugViewauthGuard

Guards

GuardAppPurpose
authGuardSharedBase auth check — waits for isInitialized, checks isAuthenticated
OrganizationGuardAdminAuth + selectedOrganisation or needsOrgSelection
OrgSelectionGuardAdminOnly allows access if needsOrgSelection is true
OrganizationAdminGuardAdminRequires OWNER or ADMIN role in selected org
WorkspaceAdminGuardAdminAsync: loads workspaces, selects by slug, checks WORKSPACE_ADMIN or org admin
loginGuardSpreadRedirects authenticated users to default workspace
authGuardSpreadChecks TokenService.hasToken(), dispatches loadWorkspaceMember

HTTP Interceptors

Admin

  1. authInterceptorFn — adds JWT token
  2. loggingInterceptorFn — logs requests/responses
  3. loadingBarInterceptorFn — controls loading bar UI
  4. loadingInterceptor — manages global loading state

Spread

  1. clientIdInterceptor — attaches X-Client-Id header for SSE echo prevention
  2. workspaceStatusInterceptor — catches 409 WORKSPACE_STATUS errors. MAINTENANCE shows error page (blocking); READ_ONLY, SCHEMA_LOCKED, DESIGN show notification toast (non-blocking)

Proxy Configuration

PatternTargetPurpose
/apihttp://localhost:8080Quarkus REST API
/ssehttp://localhost:8081SSE events

Configured in proxy.conf.json, auto-applied by Angular dev server.

Styling

Uses Material Design 3 design tokens (var(--mat-sys-primary), etc.) with two layers:

  1. Angular Material components — buttons, inputs, dialogs, tables, etc.
  2. Custom utility classes in _utilities.scss — page layout, settings cards, form grids, stat cards, status badges, role badges, avatar stacks, snackbars, and more (~50+ utility classes)

Both projects share the same utility stylesheet.

Testing

TypeCountLocation
Unit/component specs~46Colocated with source (*.spec.ts)
E2E specs11 filese2e/ directory

E2E tests cover: smoke, auth, logout, user menu, org switcher, personal settings, registration, email verification, registration flow, member invitations, accept invitation.

Unit tests use HttpTestingController for HTTP mocking and NgRx provideMockStore for state. E2E tests use data-testid attributes for stable selectors.

SchemaStack Internal Developer Documentation