Appearance
Frontend Overview
The SchemaStack frontend lives in the ../schemastack-fe repository. It's an Angular 20 multi-project workspace.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Angular 20.3.6 (standalone components) |
| State management | NgRx 20.0.1 (effects, entity, store-devtools) |
| UI framework | Angular Material 20.2.9 (Material Design 3) |
| Styling | SCSS + Tailwind CSS 3.4.18 + custom utility classes |
| Unit testing | Karma + Jasmine |
| E2E testing | Playwright 1.56.1 |
| Build | Angular 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 configurationAdmin
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:
| Category | Exports |
|---|---|
| Auth | AuthService, TokenService, LoginDialogComponent, auth interceptor, auth guard |
| Notification | NotificationService, NotificationSnackbarComponent |
| Loading | LoadingBarService, loading bar component + interceptor |
| Dialogs | ConfirmActionDialogComponent |
| Services | NetworkStatusService |
| Utilities | extractErrorMessage(), 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;
}| Slice | Key State | Effects |
|---|---|---|
auth | user, token, selectedOrganisation, availableOrganisations, needsOrgSelection, requires2FA, tempToken | Login, logout, org selection, 2FA verification |
organization | Organisation CRUD state | Organisation management |
workspace | Workspace CRUD state | Workspace CRUD + workspace-sse.effects.ts for SSE events |
loading | Global 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-state | Purpose |
|---|---|
| View metadata | currentView, allViews, viewNotFound, workspaceNotFound |
| Columns | baseColumns (from backend, for reset), columns (current), displayedColumns (ordered keys) |
| Data | viewData (row records), totalRecords, pageSize (100), loadedPages (virtual scrolling) |
| Sort/Filter | sortRules, filterRules, filterPanelOpen |
| Selection | Three modes (none, individual, all), uses selectedRowIds/excludedRowIds for efficient million-row selection |
| Bulk operations | Job tracking: jobId, operationType, status, progress, errors (max 100), downloads |
| Presets | allPresets, activePreset, hasUnsavedChanges, dialog states |
| Pending rows | SSE batching queue (see adaptive batching below) |
| Add row | Form schema, validation errors |
| Constraints | Column + entity constraints |
| Column locks | Tracks schema-altering operations in progress (ALTER TABLE) |
| Properties panel | Stack-based navigation with modes (default, edit-column, edit-view, add-relationship-column) |
| Optimistic updates | pendingOperations for view/column create/update/delete |
Spread View Effects (13 classes)
| Effect | Handles |
|---|---|
ViewLoadEffects | Loading view data, pagination |
ViewCrudEffects | View create, update, delete |
ColumnCrudEffects | Column create, update, delete, reorder. Presentation-only changes (displayName, hidden, width, position) stay FE-only |
ColumnSchemaEffects | Schema changes (handles 200 sync vs 202 async responses) |
ColumnGroupingEffects | Column display groups |
FilterSortEffects | Filter and sort — triggers data reload on change |
SelectionBulkEffects | Row selection, bulk delete/update/export |
SSERoutingEffects | Routes SSE events to NgRx actions, includes adaptive batching |
PresetEffects | Filter preset CRUD |
CellEditEffects | Single cell, bulk cell, batch cell edits |
RowCrudEffects | Add row dialog, row creation |
RelationshipEffects | Relationship picker, add related column |
ConstraintEffects | Column 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
Uint8Arraychunks, buffers text, splits on\n\n, extractsdata: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.rowsinstead 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
| Path | Component | Guard |
|---|---|---|
/dashboard | Dashboard | OrganizationGuard |
/organization/** | Lazy-loaded org routes | OrganizationGuard |
/settings/personal|security|notifications | Settings pages | authGuard |
/workspaces/** | Lazy-loaded workspace routes | OrganizationGuard |
/:workspaceSlug/workspace/** | Workspace detail | OrganizationGuard |
/auth/** | Login, register, verify | Unauthenticated layout |
/account-recovery | Account recovery | — |
/confirm-deletion/:token | Confirm account deletion | — |
Spread Routes
| Path | Component | Guard |
|---|---|---|
/login | Login | loginGuard (redirects authenticated users) |
/:orgSlug/:workspaceSlug/:viewSlug | View | authGuard |
/:orgSlug/:workspaceSlug | View | authGuard |
Guards
| Guard | App | Purpose |
|---|---|---|
authGuard | Shared | Base auth check — waits for isInitialized, checks isAuthenticated |
OrganizationGuard | Admin | Auth + selectedOrganisation or needsOrgSelection |
OrgSelectionGuard | Admin | Only allows access if needsOrgSelection is true |
OrganizationAdminGuard | Admin | Requires OWNER or ADMIN role in selected org |
WorkspaceAdminGuard | Admin | Async: loads workspaces, selects by slug, checks WORKSPACE_ADMIN or org admin |
loginGuard | Spread | Redirects authenticated users to default workspace |
authGuard | Spread | Checks TokenService.hasToken(), dispatches loadWorkspaceMember |
HTTP Interceptors
Admin
authInterceptorFn— adds JWT tokenloggingInterceptorFn— logs requests/responsesloadingBarInterceptorFn— controls loading bar UIloadingInterceptor— manages global loading state
Spread
clientIdInterceptor— attachesX-Client-Idheader for SSE echo preventionworkspaceStatusInterceptor— catches 409WORKSPACE_STATUSerrors.MAINTENANCEshows error page (blocking);READ_ONLY,SCHEMA_LOCKED,DESIGNshow notification toast (non-blocking)
Proxy Configuration
| Pattern | Target | Purpose |
|---|---|---|
/api | http://localhost:8080 | Quarkus REST API |
/sse | http://localhost:8081 | SSE 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:
- Angular Material components — buttons, inputs, dialogs, tables, etc.
- 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
| Type | Count | Location |
|---|---|---|
| Unit/component specs | ~46 | Colocated with source (*.spec.ts) |
| E2E specs | 11 files | e2e/ 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.