Skip to content

Roadmap / Backlog ​

Planned features and improvements that are not yet scheduled.

Administration Tool ​

An internal admin tool/dashboard for operational tasks that require manual intervention or bulk processing.

Planned Capabilities ​

  • Full invoice reconciliation — Sync all invoices from the payment provider (LemonSqueezy) to the local database for all customers. Needed to recover from missed webhooks or initial backfill.
  • Full subscription reconciliation — Verify all local subscription states match the payment provider.
  • Webhook replay — Re-process failed webhook events from the payment_webhook_event table.
  • Customer data sync — Bulk update local payment customer records from provider data.

Architecture Notes ​

  • Cannot run as @Scheduled jobs in Lambda (cold start / execution time limits).
  • Should live in the consumer-worker as HTTP endpoints, triggered externally (e.g. CloudWatch Events, admin UI, or CLI).
  • Full reconciliation can be slow (iterates all customers, calls provider API per customer) — must handle timeouts and partial progress.
  • Consider: internal REST endpoints on consumer-worker gated by API key or internal network.

Tier Limit Enforcement ​

When a subscription is cancelled/expired and the organisation reverts to the free plan, we need to enforce free tier limits on the product.

Needed ​

  • Workspace limits — If the org exceeds the free tier's max workspaces, block creating new ones. Consider: read-only mode for excess workspaces vs. hard-blocking access.
  • View limits — If the org exceeds max views per workspace, block creating new ones. Existing views should remain accessible (read-only or full access TBD).
  • Member limits — If the org exceeds max members, block new invites. Existing members keep access.
  • API/MCP key limits — Enforce any free tier restrictions on API keys, MCP keys, or usage quotas.
  • Grace period — Consider a grace period after downgrade before enforcing limits, giving users time to adjust or re-subscribe.
  • UI communication — Show clear banners/warnings when approaching or exceeding limits, with upgrade CTAs.

Date/DateTime Timezone Display ​

Support timezone display modes for DATE and DATETIME widget types via widgetOptions.timezoneDisplay.

Display Modes ​

ValueBehaviour
"local"Convert to user's timezone (from userTimezone)
"utc"Display as UTC
"original"Show as stored in the database (default when option is absent)

Implementation ​

No backend changes required — widgetOptions is a free-form JSONB field that passes through any key/value pairs.

Frontend needs to:

  1. Properties panel — Add a "Timezone Display" dropdown to DATETIME/DATE widget options (local / UTC / original)
  2. Save via PATCH — Send the option in widgetOptions:
    PATCH /api/columns/{columnUuid}
    { "widgetOptions": { "timezoneDisplay": "local" } }
  3. Render in spread table — Read timezoneDisplay from column metadata and format dates accordingly
  4. Add row / edit dialogs — Respect the display mode when showing date inputs

Angular 20 → 22 ​

The frontend is two majors behind: @angular/core 20.3.6, latest 22.1.4. Angular does not support skipping majors, so this is 20→21 then 21→22, one ng update per step.

Compatibility — checked 2026-08-29, nothing blocks it ​

Every third-party package already declares Angular 22 support, which is normally where an Angular upgrade dies:

PackagePeer requirement
@angular/material / @angular/cdk 22.1.4@angular/core ^22.0.0 || ^23.0.0
@ngrx/* 22.0.0@angular/core ^22.0.0
@fortawesome/angular-fontawesome 5.1.0@angular/core ^22.0.0
ngx-markdown@angular/core ^22.0.0

Node 22.22 and TypeScript 5.9.3 both satisfy Angular 22. 43 packages are outdated in total, nearly all of them in these lockstep families.

The actual risk is visual, not compilation ​

Four families move together (Angular core/CLI, Material+CDK, NgRx, fontawesome) and cannot be staged apart. But the thing to watch is that a Material major can rename MD3 design tokens, and CLAUDE.md requires every colour to come from those tokens — so a rename lands as a site-wide visual change that no unit test catches. The Playwright e2e suite asserts behaviour, not appearance, so it will stay green through a broken palette.

Six third-party packages need looking at beyond their peer ranges: ngx-tiptap, ngx-image-cropper, ngx-markdown, @bugsnag/plugin-angular, angular-eslint, ng-packagr.

Suggested approach ​

  1. 20 → 21 only, as its own change. ng update @angular/core@21 @angular/cli@21, then the Material/CDK, NgRx and fontawesome majors for that step.
  2. Run npm test (Karma) and the full Playwright e2e — npm run e2e:run.
  3. Walk the admin and spread apps by eye for token/palette regressions, because that is the failure mode the tests cannot see.
  4. Stop. Ship it. Repeat for 21 → 22.

Deliberately two increments with a checkpoint between, rather than two majors deep with no clean rollback point.

Config that reads as control but isn't ​

Three separate cases have now been found where a property or setting looks like it configures something and is silently ignored. None broke anything; all three would have misled someone reading the config to understand the system. Grouped because the class of problem matters more than the individual fixes.

1. quarkus.liquibase.change-log on metadata-rest (found 2026-08-31, production) ​

WARN [ConfigRecorder] Build time property cannot be changed at runtime:
 - quarkus.liquibase.change-log is set to 'db/changelog/changelog-messaging.xml'
   but it is build time fixed to 'db/changelog/changelog-metadata.xml'

No migration is being skipped — changelog-metadata.xml includes changelog-messaging.xml on line 9, so the messaging changesets run either way. The warning is real but the consequence is cosmetic.

The cause is a library module setting an application-global property:

ModuleSets quarkus.liquibase.change-log to
metadata-persistencechangelog-metadata.xml — correct for metadata-rest, and what the build fixed
messaging-infrastructurechangelog-messaging.xml — leaks onto every app that depends on it

messaging-infrastructure is a library, reachable from metadata-rest via metadata-producer. Its application.properties is three lines and two of them (migrate-at-start, change-log) are decisions that belong to the application, not to a shared jar. Quarkus recorded metadata's value at augmentation and resolves messaging's at runtime, so the two disagree and it warns.

Fix (S): delete both Liquibase lines from messaging-infrastructure/src/main/resources/application.properties. Nothing else consumes them — the messaging changelog is already included by the metadata one, and any app that genuinely needs it standalone should say so in its own config. Check organisation-rest and consumer-worker for the same warning while there.

2. queue.x-single-active-consumer (found 2026-08-29, fixed) ​

Set on five channels under a name the smallrye-rabbitmq connector does not recognise, so single-active-consumer had never been in force despite a comment claiming it was. Corrected for task-completion during the DLQ window. Four channels still carry the ineffective key — workspace-events-in, org-sse-member-events-in, org-events-in, migration-progress-in — each now annotated in place with a NOTE: line. Changing a queue argument means delete-and-recreate, so this needs a maintenance window; the runbook for it is at the top of metadata-consumer/src/main/resources/application.properties.

3. quarkus.log.file.enable — deprecated ​

Cosmetic; remove when next touching that config.

The pattern to check for: a property whose effect nobody has ever observed. All three were invisible until something forced a look. Worth a sweep of build-time-fixed Quarkus properties set at runtime, and of queue.* keys against the connector's actual accepted set.

Hibernate HHH90003004 — 776 warnings per 12h ​

metadata-rest logs firstResult/maxResults specified with collection fetch; applying in memory roughly 65 times an hour. Traced to Panache's firstResult(), which sets maxResults(1); when the query also LEFT JOIN FETCHes a collection, Hibernate cannot push the limit into SQL and paginates the materialised rows instead.

The dominant source is McpApiKeyRepository.findActiveByKeyHash — a LEFT JOIN FETCH k.viewScopes plus .firstResult() that runs on every MCP request, so the volume tracks MCP traffic and likely rose with the 30 Aug auth cutover (the mechanism now resolves the key on every call). Same shape at findByUuidAndWorkspaceIdWithScopes and at WorkspaceRepository.findByUuidWithViews / findByUuidWithUsers / findBySlugAndOrganisationIdWithViews.

This is log noise, not a slow query. Every one of these filters on a unique column (keyHash, uuid), so at most one root entity matches and "in memory" means limiting a one-row list to one row. The cost is that 776 warnings per 12h bury anything real — the same reason the router error took three attempts to spot.

Fix (S): where the predicate is already unique, the limit is redundant — replace .firstResult() with .list() and take element 0. Identical semantics, no implicit maxResults, no warning. Do not blanket-suppress the logger: the warning is correct and would be worth seeing on a query that genuinely returns many roots.

MCP: what's left after the 30 Aug audit ​

The full 56-tool guard matrix was re-extracted from deployed code on 30 Aug; residuals #1 and #2 from the 26 Aug audit were already fixed by 80f2e8a4, and the ceiling-drift risk was closed by 952a3df0. What remains:

  • Preset owner-substitution — FilterPresetService.getCurrentUser() resolves every MCP caller to the organisation OWNER, because FilterPreset.createdBy is NOT NULL and a machine credential has no user row. A fix is written but uncommitted (see below): McpPresetAccess restricts MCP callers to SHARED presets for both see and modify, and refuses toggleFavorite outright as personal state. Owner attribution on creation remains — that is the data-model constraint, not the hole.
  • Preview naming sibling views — affectedViewUuids in a migration preview enumerated views outside a scoped key's scope. Fix written but uncommitted: scoped keys see only in-scope views, while affectedFkTables stays complete so blast radius is still honest.
  • ToolFilter and @Resource — both unblocked by the auth cutover (identity now exists before dispatch) and both still to build. ToolFilter is a synchronous CDI bean reading the resolved identity; @Resource needs design decisions on URI scheme and how view scoping applies to resources.

CI — written, uncommitted ​

.github/workflows/backend-tests.yml runs the Quarkus and Spring suites on push to main and on PRs. Two guards matter and should survive any edit:

  • an assertion that 2,000+ tests actually executed — the POM sets <skipTests>true</skipTests>, so a plain mvn test passes having run nothing. Without this guard CI would report green on an empty run, which is worse than no CI;
  • scripts/detect-vacuous-tests.py — 308 test methods once asserted nothing because nested UniAsserter registrations are silently dropped.

Deliberately not a deploy gate — deploys run from a laptop via blue/green, and wiring CI into that is a separate decision.

Uncommitted work as of 2026-08-31 ​

The three items above (preset authority, preview scope filter, CI workflow) are written and unit-tested but not committed, and their full gate did not complete — it died on a corrupted target/ class file (debris from concurrent Maven runs), and the rebuild afterwards was never followed by a re-run. Re-run the metadata + organisation gate before committing.

SchemaStack Internal Developer Documentation