Skip to content

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.ts export
  • 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

LayerPatternExample
Store file<action>-<module>.stores.tsfetch-bulletin.stores.ts
Service file<module>.service.tsbulletin.service.ts
API file<module>.api.tsbulletin.api.ts
Type file<module>.types.tsbulletin.types.ts
Component filePascalCaseBulletinCard.svelte
Store subdirectory<action>-<module>/super-user-entry/
Barrel exportindex.tsre-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 tests

Step Control Store

form-setup-step-control.stores.ts manages wizard navigation:

MethodDescription
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_FOLDERS env 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.json and lang/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: use import type for type-only imports
  • import/order: alphabetized imports
  • curly: all: always use curly braces
  • Padding lines around control statements (if, return, for, etc.)