SkillAgentSearch skills...

architecture-vertical-slice

Vertical Slice Architecture — layers, import rules, and slice structure for src/main and src/preload

Install / Use

npx skills add AK2083/DojoSphere

Installs into whichever agent you are using.

About this skill
🔧

.clinerules

Cline rules

Quality Score

66/100

Supported Platforms

Cline

Tags


description: Vertical Slice Architecture — layers, import rules, and slice structure for src/main and src/preload globs: src/main//*,src/preload//* alwaysApply: false

Vertical Slice Architecture (Main & Preload)

The Electron main process and preload bridge use vertical slices — not FSD. The renderer follows architecture-fsd.md; main and preload are separate entry points at the same level under src/.

src/
  renderer/   → FSD (UI, features, pages)
  main/       → Vertical Slices (IPC, SQLite, domain logic)
  preload/    → IPC bridge (contextBridge → window.api)

Layers

Imports point only inward — from app to features/shared, from features to shared. Features may call other features only via their public API (index.ts).

app → features, shared
features → shared (+ other features only via index.ts)
shared → internal only
window / preload → Electron shell (outside slices)

| Layer | Path | Responsibility | | ------------ | ---------------------------- | ----------------------------------------------------------------------- | | app | src/main/app/ | Composition root: bootstrap, IPC aggregation (register-ipc.ts) | | features | src/main/features/<slice>/ | One use case / bounded context per slice | | shared | src/main/shared/ | Infrastructure without business logic (database, security helpers) | | window | src/main/window/ | BrowserWindow, renderer loading | | preload | src/preload/ | contextBridge.exposeInMainWorld('api', …) — no SQL, no auth decisions |

app

Composition root of the main process — starts the app and wires slices.

  • bootstrap.ts — initialize database, open window, register IPC
  • register-ipc.ts — calls only registerXxxIpc() from feature index.ts
  • No SQL queries, no business logic — orchestration only
  • main.ts (entry) stays thin and delegates to app/

features

Business functionality in slices — one slice = one cohesive use case in the main process (e.g. users, sessions, health).

Ideal slice structure:

src/main/features/<slice>/
  ipc/
    register.ts       # ipcMain.handle — thin adapter, no SQL
  service/            # use-case orchestration (optional)
  repository/         # SQL access (optional)
  index.ts            # public API — sole export path for other slices
  *.test.ts           # tests colocated with code

Layering within a slice:

| Folder | Role | | ----------------- | -------------------------------------------------------------------------------- | | ipc/register.ts | Channel names, input validation, session check, delegation to service/repository | | service/ | Coordinate multiple repositories/slices (e.g. create user + session) | | repository/ | SQL via @main/shared/database — no electron, no ipcMain |

Existing slices (orientation): health, system, authorization, sessions, users.

  • Cross-slice dependencies only via @main/features/<slice> (index.ts), never via internal paths like another slice's repository/….
  • Add new functionality as a new slice; do not mix into existing slices.

shared

Infrastructure without feature affiliation. Features import only the public API.

| Segment | Content | | --------------------------- | -------------------------------------------------------------------- | | shared/database/ | SQLite port, runtime, migrations — see subdivision below | | shared/database/driver.ts | Sole place with node:sqlite — do not import directly from features | | shared/security/ | e.g. requireActiveSession() for privileged IPC handlers |

shared/database/ — recommended subdivision

shared/database is not a feature slice but technical infrastructure (port + adapter). Still, a clear split by responsibility, not domain, is worthwhile:

shared/database/
  index.ts              # public API — sole import path for features
  types/
    database.ts         # Database, DatabaseStatement
    migration.ts        # Migration
  connection.ts         # initDatabase, getDatabase, closeDatabase, createMemoryDatabase
  transactions.ts       # runInTransaction
  pragmas.ts            # SQLite PRAGMAs
  runner.ts             # runMigrations
  validate-schema.ts    # schema assertions after migrations (no DDL)
  driver.ts             # SQLite adapter — sole import of node:sqlite
  migrations/           # versioned .sql files + registry (index.ts)

| Segment | Responsibility | Imported by | | ---------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------- | | Types (types/) | Port interfaces | Driver, runtime modules, features (types only) | | Driver (driver.ts) | Concrete SQLite binding | connection.ts only | | Runtime (connection, transactions, pragmas) | Connection, transactions, PRAGMAs | app/bootstrap, features via public API | | Migration (runner.ts, validate-schema.ts, migrations/) | Runner, schema assertions, versioned SQL | app/bootstrap |

Rules:

  • Features/repositories import only @main/shared/database (getDatabase, runInTransaction) — never driver.ts or other internal modules directly.
  • No domain-specific DDL outside migrations/*.sql — tables live in versioned migrations (e.g. users in V001__authorize_create_tables.sql). validate-schema.ts only asserts, throws on incompatible legacy schema.
  • Schema ownership: SQL files named by slice/context (authorize_*, users_*, …), registry in migrations/index.ts aggregates all files. Optional later: SQL per feature (features/<slice>/migrations/) exported and collected in registry — only worthwhile when many slices maintain their own schema.

Slice vs. shared boundary:

| What | Where | | --------------------------------------- | ---------------------------------------------- | | CREATE TABLE users, columns, indexes | shared/database/migrations/*.sql | | SELECT/INSERT/UPDATE per use case | features/<slice>/repository/ | | Session check before write access | shared/security or slice ipc/ | | Open DB, migrate, close | app/bootstrap + shared/database public API |

  • shared imports no features and no electron except where technically required (database path via app).
  • Tests use @main/shared/database, not internal driver paths (driver.ts).

window

Electron window setup — outside feature slices.

  • main-window.tsBrowserWindow, preload path, DevTools rules
  • load-renderer.ts — dev server vs. dist for the renderer
  • No domain logic, no IPC handling

preload

Lives at the same level as main/ and renderer/ (src/preload/).

  • Exposes typed methods on window.api (see src/renderer/shared/types/electron-api.ts)
  • Calls only ipcRenderer.invokeno privileged decisions, no SQLite
  • New IPC channels: extend preload.ts + electron-api.ts + main slice ipc/register.ts together

Import rules

  1. features → shared — allowed (@main/shared/database, @main/shared/security)
  2. features → features — only via @main/features/<slice> (index.ts)
  3. shared → features — forbidden
  4. app → features — public APIs only (registerXxxIpc, exported services)
  5. Repositories — no electron, no ipcMain
  6. IPC handlers — no SQL, no complex business logic (delegate to service/repository)
  7. Renderer — accesses main only via window.api (preload), never main code directly

Security

  • Authorization and session checks only in the main process (auth-security.md, security-privacy.md)
  • Privileged IPC handlers: session (permissions later) via @main/shared/security or session slice
  • Store session tokens as hashes only — never plaintext
  • SQLite only in the main process via @main/shared/database

Import aliases

  • @main/app/* — composition root
  • @main/features/<slice> — slice public API
  • @main/shared/* — shared infrastructure

Preload imports types from @shared/types/electron-api (renderer shared types — intentional exception for the API contract layer).

Documentation

  • All exported functions, types, and interfaces under src/main/ require English JSDoc.
  • Enforced via ESLint (jsdoc/* on src/main/**/*.ts, excluding tests).
  • Internal helpers without export are documented only when non-obvious.

Renderer (FSD) vs. main (VSA)

| Aspect | Renderer (src/renderer/) | Main (src/main/) | | -------------- | -------------------------- | ----------------------------------- | | Architecture | Feature-Sliced Design | Vertical Slices | | UI | ui/, Vue components | No UI (except window/) | | Data access | IPC → window.api | SQLite via shared/database | | Auth decisions | Never alone in renderer | In main process | | Public API | index.ts per slice | index.ts per slice + IPC channels |

Checklist for new main slices

  1. Correct slice chosen — one use case, not a catch-all folder?
  2. src/main/features/<slice>/index.ts as sole export surface?
  3. IPC registered in src/main/app/register-ipc.ts?
  4. Preload + electron-api.ts extended only for new IPC channels?
  5. Schema change → migration under src/main/shared/database/migrations/?
  6. Tests colocated; DB tests via @main/shared/database?
  7. Privileged handlers secured with session check?
  8. Exported APIs documented with English JSDoc?

Related Skills

View on GitHub
GitHub Stars0
CategoryDevelopment
UpdatedNaNy ago
Forks0

Security Score

68/100

Audited on Invalid Date

2 medium1 low