Development Conventions
Store Architecture (stores → services → api)
All data flow follows this strict layering:
Component (Svelte)
│ subscribe() + call action
▼
Store (state management)
│ delegate business logic
▼
Service (orchestration)
│ HTTP calls
▼
API (fetch)
│ network
▼
Backend (Laravel)Rule: Never Skip Layers
- A component calls a store action, NOT a service/API directly
- A store calls a service, NOT the API directly
- A service calls the API, NOT fetch directly (unless it's an SSE stream)
Component Rules
Components are presentational only — this applies to atoms, molecules, AND organisms:
svelte
<!-- ✅ CORRECT: import store, subscribe, call action -->
<script lang="ts">
import { onMount } from 'svelte';
import { myStore } from './stores/my.stores';
let data = $state(myStore.initialData);
onMount(() => {
return myStore.subscribe((s) => { data = s.data; });
});
function handleClick() {
myStore.doAction();
}
</script>svelte
<!-- ❌ WRONG: raw fetch, SSE parsing, business logic in component -->
<script lang="ts">
async function handleClick() {
const res = await fetch('/api/foo'); // NO!
// ...
}
</script>Store Files
- Format:
<action>-<module>.stores.ts(e.g.,fetch-bulletin.stores.ts,super-user-form.stores.ts) - Group related stores in subdirectories:
stores/super-user-entry/,stores/otp-entry/ - Each subdirectory has a barrel
index.tsexport - Every store uses
StateContainer<TData>from@/types/state
typescript
// ✅ CORRECT store pattern
export interface MyData { items: string[]; sending: boolean; }
export type MyContainer = StateContainer<MyData>;
class MyStore {
#state: MyContainer = createInitialState();
#listeners = new Set<Listener>();
get state() { return this.#state; }
subscribe(l) { ... }
doAction() { this.#emit(); }
#emit() { ... }
}Service Files
- Format:
<module>.service.ts - Pure functions, no state
- Bridge between stores and API
API Files
- Format:
<module>.api.ts - Only HTTP fetch calls
- Return raw response data, typed
Naming Conventions
| Layer | Pattern | Example |
|---|---|---|
| Store file | <action>-<module>.stores.ts | fetch-bulletin.stores.ts |
| Service file | <module>.service.ts | bulletin.service.ts |
| API file | <module>.api.ts | bulletin.api.ts |
| Type file | <module>.types.ts | bulletin.types.ts |
| Component file | PascalCase | BulletinCard.svelte |
| Store subdirectory | <action>-<module>/ | super-user-entry/ |
| Barrel export | index.ts | re-exports all |
- Frontend: camelCase (
firstName,setField) - Backend communication: snake_case (
first_name,set_field) - Event handlers: use function references (
onclick={handleClick}) not inline arrows when possible
Module Structure
module/
├── Index.svelte # Entry point — minimal script, pure composition
├── api/ # API functions
├── parts/ # Sub-components (atomic design)
│ ├── ComponentA.svelte
│ └── ComponentB.svelte
├── services/ # Business logic
├── stores/ # State management
│ ├── action-module.stores.ts
│ └── subdir/
│ ├── store-a.stores.ts
│ └── index.ts
├── types/ # TypeScript interfaces
└── __tests__/ # Vitest testsStep Control Store
form-setup-step-control.stores.ts manages wizard navigation:
| Method | Description |
|---|---|
next() | Advance to next step |
prev() | Go back one step |
goToStep(n) | Jump to specific step |
configure(total, initial) | Set total + current step |
configureFromChecks(checks) | Auto-calculate starting step |
Important: Callback registration (like registerAboutSave) does NOT belong in the store. Store those callbacks as local variables in the parent component.
Auto-Navigation
When visiting /setup, the wizard auto-advances to the first incomplete step:
typescript
// ✅ CORRECT: compute immediately, not in $effect
let currentStep = $state(computeInitialStep(initialChecks));Never set let currentStep = $state(1) and then update in an effect — this causes render flash.
Migration Runner
The custom runner at database/migrations/0000_runner_migration.php:
- Reads
MIGRATION_RUNNER_FOLDERSenv var (default:v1_0_0,...) - Runs migrations in version subdirectories
- Supports custom sort order via
sortMigrations()overrides - Outputs folder name during migration:
echo "--- Migrating: v1_0_0 ---"
Multi-Language (i18n)
- Translations stored in
lang/en.jsonandlang/id.json - Import
{ t }from@/lib/i18n.svelte - Key pattern:
<module>.<section>.<key>(e.g.,setup.seed.title) - Backend sends translations via Inertia props
- Never hardcode display strings in components
ESLint Rules
consistent-type-imports: useimport typefor type-only importsimport/order: alphabetized importscurly: all: always use curly braces- Padding lines around control statements (
if,return,for, etc.)