refactor: remove extension toggle system — compiled-in extensions are always active (#59)
The runtime toggle system (extension_toggles table, API routes, hooks, UI components) added unnecessary complexity. Extensions controlled via extensions.config.json at build time are now always active for all users. This removes ~835 lines of toggle-related code including API routes, DB queries, the ExtensionToggleButton component, useEnabledExtensions and useExtensionToggle hooks, and the toggle-check module. AI consent gating remains unchanged. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
38b658205d
commit
cf77adaa0a
+36
-60
@@ -21,13 +21,13 @@ That's the core. It doesn't include receipt scanning, AI categorization, AI chat
|
||||
|
||||
## Extensions
|
||||
|
||||
Extensions are **everything beyond the core accounting system**. They are self-contained tools that a user adds to their dashboard. No extensions are active by default — the user chooses which ones they want.
|
||||
Extensions are **everything beyond the core accounting system**. They are self-contained tools that a user adds to their dashboard. All compiled extensions are active for all users — the operator decides which extensions to include via `extensions.config.json` at build time.
|
||||
|
||||
There are two kinds of extensions:
|
||||
|
||||
### General Extensions
|
||||
|
||||
General extensions are not tied to any specific business sector. They're useful for any business but they go beyond what a standard accounting system offers. They are optional — the user toggles them on from the marketplace.
|
||||
General extensions are not tied to any specific business sector. They're useful for any business but they go beyond what a standard accounting system offers.
|
||||
|
||||
Examples:
|
||||
- **Receipt OCR** — Scan receipts and extract data automatically
|
||||
@@ -36,7 +36,7 @@ Examples:
|
||||
- **Push Notifications** — Event notifications for accounting activities
|
||||
- **Enable Banking** — PSD2 automatic bank transaction sync
|
||||
|
||||
These are configured via `extensions.config.json` and only loaded when explicitly enabled. Users toggle them on/off from the marketplace.
|
||||
These are configured via `extensions.config.json` and only loaded when explicitly enabled by the operator.
|
||||
|
||||
### Sector Extensions
|
||||
|
||||
@@ -142,13 +142,12 @@ Some extensions need data that doesn't exist in any accounting system. No system
|
||||
**Pattern C: Both**
|
||||
Some extensions combine core accounting data with user-submitted data. "Earnings Per Alcohol Liter" reads alcohol revenue from the bookkeeping (Pattern A) and takes user-entered liter counts (Pattern B) to calculate revenue per liter.
|
||||
|
||||
### 3. Full marketplace for post-onboarding management
|
||||
### 3. Extension marketplace for browsing
|
||||
|
||||
After onboarding, users have a dedicated "Extensions" marketplace page where they can:
|
||||
- Browse all available extensions (general + all sectors)
|
||||
Users have a dedicated "Extensions" marketplace page where they can:
|
||||
- Browse all compiled extensions (general + all sectors)
|
||||
- Read descriptions and details
|
||||
- Toggle extensions on/off at any time
|
||||
- Discover extensions from sectors other than their primary one
|
||||
- Open extension workspaces
|
||||
|
||||
### 4. Primary sector with cross-sector browsing
|
||||
|
||||
@@ -168,13 +167,10 @@ We build all extensions ourselves initially. But the architecture should be clea
|
||||
## The User Experience
|
||||
|
||||
1. User signs up, goes through onboarding
|
||||
2. During onboarding, they select their business sector ("Restaurant & Cafe")
|
||||
3. The app suggests extensions: general extensions + extensions for that sector
|
||||
4. User toggles on the ones they want
|
||||
5. On the dashboard, the sidebar has a **"Your Extensions"** section listing all enabled extensions
|
||||
6. Clicking an extension opens its workspace — a dedicated page with the extension's own UI
|
||||
7. The user interacts with the extension: views data, enters inputs, sees calculations/reports
|
||||
8. User can browse the marketplace anytime to add/remove extensions
|
||||
2. On the dashboard, the sidebar shows links to compiled extensions that have a workspace + quickAction
|
||||
3. Clicking an extension opens its workspace — a dedicated page with the extension's own UI
|
||||
4. The user interacts with the extension: views data, enters inputs, sees calculations/reports
|
||||
5. User can browse the marketplace to see all compiled extensions
|
||||
|
||||
---
|
||||
|
||||
@@ -381,15 +377,14 @@ lib/
|
||||
types.ts ← Extension, ExtensionDefinition, Sector types
|
||||
sectors.ts ← Sector shells + generated extension definitions
|
||||
workspace-registry.tsx ← Maps sector/slug → lazy-loaded React component
|
||||
hooks.ts ← useExtensionToggle, useEnabledExtensions
|
||||
loader.ts ← Imports from _generated, registers extensions
|
||||
registry.ts ← Runtime extension registry (get, register)
|
||||
toggle-check.ts ← isExtensionEnabled() for auth gates
|
||||
context-factory.ts ← Builds ExtensionContext for handlers
|
||||
_generated/ ← AUTO-GENERATED by npm run setup:extensions
|
||||
extension-list.ts ← FIRST_PARTY_EXTENSIONS array (static imports)
|
||||
workspace-map.tsx ← Lazy-loaded workspace components
|
||||
sector-definitions.ts ← ExtensionDefinition[] per sector
|
||||
enabled-extensions.ts ← ENABLED_EXTENSION_IDS set for build-time checks
|
||||
email/
|
||||
service.ts ← EmailService interface + no-op default + getEmailService()
|
||||
reports/
|
||||
@@ -435,7 +430,7 @@ app/(dashboard)/
|
||||
[sector]/
|
||||
page.tsx ← Extensions for a specific sector
|
||||
[extension]/
|
||||
page.tsx ← Extension detail + toggle
|
||||
page.tsx ← Extension detail + workspace link
|
||||
e/ ← Extension workspaces
|
||||
[sector]/
|
||||
[slug]/
|
||||
@@ -461,27 +456,23 @@ key: 'entries' → [{ "date": "2025-01-15", "liters": 42.5, "type": "spirit
|
||||
key: 'config' → { "revenueAccounts": ["3001"], "trackByType": true }
|
||||
```
|
||||
|
||||
### The Toggle System
|
||||
### Extension Enablement
|
||||
|
||||
New database table:
|
||||
Extensions are enabled at **build time** via `extensions.config.json`. All compiled extensions are active for all users — there is no per-user toggle system. The operator (hosted or self-hosted) decides which extensions to include.
|
||||
|
||||
```sql
|
||||
create table extension_toggles (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid not null references auth.users on delete cascade,
|
||||
sector_slug text not null, -- 'general' | 'restaurant' | 'construction' | etc.
|
||||
extension_slug text not null,
|
||||
enabled boolean not null default true,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint extension_toggles_unique unique (user_id, sector_slug, extension_slug)
|
||||
);
|
||||
To check if an extension is compiled in at runtime (e.g. for conditional UI), use the build-time constant:
|
||||
|
||||
```typescript
|
||||
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
|
||||
|
||||
if (ENABLED_EXTENSION_IDS.has('receipt-ocr')) {
|
||||
// Show OCR UI
|
||||
}
|
||||
```
|
||||
|
||||
Also add to company_settings:
|
||||
```sql
|
||||
alter table company_settings add column sector_slug text;
|
||||
```
|
||||
AI extensions (`receipt-ocr`, `ai-categorization`, `ai-chat`) additionally require per-user AI consent before making API calls. This is a separate system using the `extension_data` table, managed by `lib/extensions/ai-consent.ts`.
|
||||
|
||||
> **Note:** The `extension_toggles` database table still exists but is no longer queried by any code. It can be dropped in a future migration.
|
||||
|
||||
### API Routes for Extensions
|
||||
|
||||
@@ -525,8 +516,8 @@ URL scheme: `/api/extensions/ext/{extensionId}/{...routePath}`
|
||||
The dispatcher handles:
|
||||
1. **Auth check** — 401 if not logged in
|
||||
2. **Extension lookup** — 404 if extension not registered or has no apiRoutes
|
||||
3. **Toggle check** — 403 if extension is disabled for the user
|
||||
4. **Path matching** — Matches method + path pattern (supports `:param` wildcards)
|
||||
3. **Path matching** — Matches method + path pattern (supports `:param` wildcards)
|
||||
4. **AI consent check** — 403 if AI extension and user hasn't consented
|
||||
5. **Param extraction** — Path params like `:id` are added as `_id` search params
|
||||
6. **Context building** — Creates `ExtensionContext` with supabase, userId, settings, storage, logger
|
||||
7. **Dispatch** — Calls the matched handler with the request and context
|
||||
@@ -541,7 +532,7 @@ app/api/extensions/[sector]/[slug]/
|
||||
|
||||
### Sidebar Integration
|
||||
|
||||
The sidebar (`DashboardNav.tsx`) gets a new section: **"Your Extensions"**. It reads enabled extensions from `extension_toggles` and renders links:
|
||||
The sidebar (`DashboardNav.tsx`) has a section for extensions. It shows all compiled extensions that have a workspace and a `quickAction` with an `href`:
|
||||
|
||||
```
|
||||
── Your Extensions ──────────
|
||||
@@ -553,21 +544,6 @@ The sidebar (`DashboardNav.tsx`) gets a new section: **"Your Extensions"**. It r
|
||||
|
||||
Each link goes to `/e/{sector}/{slug}` which renders the extension's workspace component.
|
||||
|
||||
### Onboarding Integration
|
||||
|
||||
Add two new steps to the onboarding flow (after entity type selection):
|
||||
|
||||
**Step 2: Sector Selection**
|
||||
"What type of business do you run?"
|
||||
Grid of sectors with icons and descriptions. User picks one.
|
||||
Stores `sector_slug` on `company_settings`.
|
||||
|
||||
**Step 3: Extension Suggestions**
|
||||
"Here are tools for your business. Pick the ones you want."
|
||||
Shows general extensions + extensions for the selected sector, grouped by category.
|
||||
User toggles desired extensions. Inserts into `extension_toggles`.
|
||||
Can be skipped — user can always add extensions later from the marketplace.
|
||||
|
||||
---
|
||||
|
||||
## The Extension Workspace Pattern
|
||||
@@ -620,7 +596,7 @@ The previous architecture had extensions "always loaded" via hardcoded static im
|
||||
|
||||
2. **Services pattern** -- Extensions can expose named services via `services?: Record<string, (...args: any[]) => Promise<any>>` on the Extension interface. Core code uses `extensionRegistry.get('ext-id')?.services?.methodName` for runtime lookup instead of direct imports. This is how `ai-categorization` provides template embedding functions to core booking logic.
|
||||
|
||||
3. **Catch-all API dispatcher** -- Extension API routes are registered via `apiRoutes: ApiRouteDefinition[]` on the Extension object. The catch-all at `/api/extensions/ext/[...path]/route.ts` handles auth, toggle checks, path param extraction, and dispatches to the handler. URL pattern: `/api/extensions/ext/{extensionId}/{path}`.
|
||||
3. **Catch-all API dispatcher** -- Extension API routes are registered via `apiRoutes: ApiRouteDefinition[]` on the Extension object. The catch-all at `/api/extensions/ext/[...path]/route.ts` handles auth, AI consent checks, path param extraction, and dispatches to the handler. URL pattern: `/api/extensions/ext/{extensionId}/{path}`.
|
||||
|
||||
4. **SRU/NE-bilaga are core** -- These tax compliance features were moved from `extensions/` into `lib/reports/sru-export/` and `lib/reports/ne-bilaga/`. They are always available regardless of extension configuration.
|
||||
|
||||
@@ -643,14 +619,14 @@ The previous architecture had extensions "always loaded" via hardcoded static im
|
||||
|------|--------|-------|
|
||||
| Extension types (ExtensionDefinition, Sector, etc.) | Done | `lib/extensions/types.ts` |
|
||||
| Sector data registry | Done | `lib/extensions/sectors.ts` + generated definitions |
|
||||
| Database migration (extension_toggles + sector_slug) | Done | Migration 037 |
|
||||
| Toggle hooks (useExtensionToggle, useEnabledExtensions) | Done | `lib/extensions/hooks.ts` |
|
||||
| Database migration (extension_toggles) | Done | Migration 037 (table exists but no longer queried) |
|
||||
| Build-time extension check | Done | `ENABLED_EXTENSION_IDS` in `_generated/enabled-extensions.ts` |
|
||||
| Workspace component registry | Done | `lib/extensions/workspace-registry.tsx` + generated map |
|
||||
| Workspace routing (`/e/[sector]/[slug]`) | Done | `app/(dashboard)/e/[sector]/[slug]/page.tsx` |
|
||||
| Workspace shell | Done | `components/extensions/ExtensionWorkspaceShell.tsx` |
|
||||
| Marketplace pages | Done | `app/(dashboard)/extensions/` |
|
||||
| Sidebar "Your Extensions" | Done | Wired into DashboardNav |
|
||||
| Onboarding steps (sector selection + extension suggestions) | Done | Onboarding flow |
|
||||
| Onboarding with build-time extension checks | Done | Uses `ENABLED_EXTENSION_IDS` |
|
||||
| Shared UI components | Done | KPICard, DataEntryForm, DateRangeFilter, etc. |
|
||||
| Extension API routes (generic CRUD) | Done | `app/api/extensions/[sector]/[slug]/` |
|
||||
| Catch-all API dispatcher | Done | `app/api/extensions/ext/[...path]/route.ts` |
|
||||
@@ -660,7 +636,7 @@ The previous architecture had extensions "always loaded" via hardcoded static im
|
||||
| Manifest files for all extensions | Done | 25 manifest.json files |
|
||||
| Code generator | Done | `scripts/generate-extension-registry.ts` |
|
||||
| extensions.config.json opt-in | Done | Core runs with empty config |
|
||||
| Migrate general extensions to toggle system | Done | All general extensions have manifests |
|
||||
| General extensions with manifests | Done | All general extensions have manifests |
|
||||
| Export sector extensions | Done | EU Sales List, Intrastat, VAT Monitor, Currency Receivables |
|
||||
| Restaurant sector extensions | Done | Food Cost, Earnings Per Liter, POS Import, Tip Tracking |
|
||||
| Construction sector extensions | Done | ROT Calculator, Project Cost |
|
||||
@@ -843,7 +819,7 @@ export const myExtension: Extension = {
|
||||
}
|
||||
```
|
||||
|
||||
Events are one-way: core services emit, extensions subscribe. Extensions should never emit events back to core. The toggle check is enforced by the event handler registration -- if an extension is not loaded (not in `extensions.config.json`), its handlers are never registered.
|
||||
Events are one-way: core services emit, extensions subscribe. Extensions should never emit events back to core. If an extension is not compiled in (not in `extensions.config.json`), its handlers are never registered.
|
||||
|
||||
### Workspace Components
|
||||
|
||||
@@ -889,6 +865,6 @@ This pattern can be reused for any capability that should degrade gracefully whe
|
||||
- **Sector extensions** (food cost %, earnings per liter, etc.) -- tied to a specific market sector
|
||||
- **Export extensions** (EU Sales List, Intrastat, etc.) -- for businesses with international trade
|
||||
|
||||
All extensions live in the same system, use the same toggle mechanism, appear in the same marketplace, and show up under "Your Extensions" in the sidebar. No extensions are active by default -- operators choose which to enable in `extensions.config.json`, and users toggle them on/off from the marketplace.
|
||||
All extensions live in the same system, appear in the same marketplace, and show up in the sidebar when they have a workspace + quickAction. All compiled extensions are active for all users -- the operator chooses which to include in `extensions.config.json` at build time. AI extensions additionally require per-user consent before making API calls.
|
||||
|
||||
Extensions are read-only with respect to the core accounting system. They can be fed accounting data, they can accept manual user input, but they never write back to the bookkeeping. They can expose services to core via the registry lookup pattern, and they can register API routes that are dispatched by the catch-all handler.
|
||||
|
||||
Reference in New Issue
Block a user