ICP clip storage app. Backend: Motoko canister. Frontend: SvelteKit SPA.
Uses icp CLI, not dfx. There is an icp.yaml in the repo (no dfx.json).
backend/main.mo— Motokopersistent actor classwith clip CRUD (create_clip,get_clip,get_stats).- Uses
mo:coreprimitives (Map, Int, Text, Blob, Time, Result, Option). - Canister init args:
?{ max_seconds_to_live: Nat; max_blob_bytes: Nat }(both optional). - Clips have TTL (default 7 days), max blob size (default 1MB), and optional
burn_after_read. create_clipis an update call;get_clipis a query call (best-effort expiry check, cannot mutate state);get_statsis an update call (iterates and computes live stats).persistent actorauto-persists alllet/vardeclarations across upgrades.
frontend/— Svelte 5 + SvelteKit 2 app, static adapter (@sveltejs/adapter-static), build output todist/.- Routes (all under
frontend/routes/):+page.svelte— Home/idle (paste, receive via QR, share).send/+page.svelte— Sender mode (receiver shares their clip ID + password hash; sender pastes text here).share/+page.svelte— Create/edit clip form (chooser mode for picking existing local clips).view/+page.svelte+view/+page.ts— Receive/decrypt clip (?clip=ID#password).list/+page.svelte+list/+page.ts— Saved clips grid (focus, maximize, edit, delete, share).faq/+page.svelte— FAQ page.
- Layout:
+layout.svelte(global styles, CSS vars, meta tags, service worker registration),+layout.ts. - Global styles & CSS vars are defined in
+layout.svelte:global()blocks.
frontend/lib/api/client.ts— Thin wrapper aroundcreateClip/fetchClipfrom$lib/icp/actor.frontend/lib/icp/actor.ts— Actor creation via@icp-sdk/core; handlesHttpAgent,Ed25519KeyIdentity,Principal, and CandididlFactoryfrom$generated/backend-did. Canister ID injected at build time viaBACKEND_CANISTER_ID(from.icp/data/mappings/ic.ids.jsonor.icp/cache/mappings/local.ids.json).frontend/lib/api/local-store.ts— IndexedDB persistence (db namecopycat, storeclips). In-memory Map cache with dirty/removed tracking. Supports legacy migration fromlocalStoragekeycopycat_clips.frontend/lib/api/store.ts— Sveltewritablestores for cross-component state:modalState,clipState,sendState,shareState,headerClipCount.frontend/lib/crypto.ts— Client-side AES-256-GCM encryption. Key derived via PBKDF2 (100k iterations, SHA-256). Ciphertext layout:salt(16) + iv(12) + ciphertext.frontend/lib/words.ts— 2020-word dictionary.generateClipId()produces human-readable 3-word IDs (word-word-word) with ~44 bits of entropy.frontend/lib/qr.ts— QR rendering with central logo overlay (kopicat-logo.png).
- Source assets:
assets/kopicat.png. Makefile resizes it intostatic/(favicons, apple-touch-icon, etc.). frontend/service-worker.ts— Workbox PWA (injectManifeststrategy). Precaches build output; images useStaleWhileRevalidatewith 30-day expiration.- CSP (production only, in
svelte.config.js):default-src 'self',script-src 'self',style-src 'self' 'unsafe-inline',connect-src 'self' https://icp-api.io.
make backend # Builds build/backend.wasm + build/backend.did (vessel + moc, version from vessel.dhall)
make frontend # Runs pnpm build (Vite static build → dist/)
make # Builds backend + frontend + assets
make assets # Resizes source images into static/
pnpm build # Vite build (frontend only)
pnpm check # svelte-check
pnpm test # vitest run (jsdom, files: frontend/**/*.test.ts)
pnpm dev # Vite dev server (frontend only, proxies /api to local replica)Always use pnpm, never npm.
icp deploy # Deploys canisters per icp.yaml (backend + frontend asset canister, must be already built)
icp sync # Syncs dist/ assets to the frontend asset canistericp.yaml specifies:
- Backend: build script runs
make backend, copiesbuild/backend.wasmto$ICP_WASM_OUTPUT_PATH. - Frontend:
@dfinity/asset-canister@v2.1.0recipe,dir: dist, build:make frontend.
build/backend.wasmbuild/backend.didbuild/backend-did.ts(Candid → TypeScript)build/backend-did.mjs(Candid → JS)
backend/main.mo— clip canister APIbackend/test.mo— standalone Motoko test snippet (HashMap path matching experiments)vessel.dhall+package-set.dhall— Motoko deps (mo:corev2.5.0) + compiler version (1.7.0)Makefile— orchestratesvessel,moc,didc, and ImageMagickmagickfor asset resizingicp.yaml—icpCLI deployment manifestfrontend/routes/+page.svelte— home pagefrontend/routes/send/+page.svelte— send flowfrontend/routes/share/+page.svelte— create/share flowfrontend/routes/view/+page.svelte+view/+page.ts— receive/decrypt flowfrontend/routes/list/+page.svelte+list/+page.ts— saved clips gridfrontend/lib/api/local-store.ts— IndexedDB clip storefrontend/lib/api/store.ts— Svelte writable storesfrontend/lib/components/ClipDisplay.svelte— shared clip viewer/editorfrontend/lib/components/GridView.svelte— clip grid with focus/maximize/chooserfrontend/lib/components/CreateForm.svelte— clip creation formfrontend/lib/components/DecryptForm.svelte— password entry for decryptionfrontend/lib/components/ResultView.svelte— decrypted clip display with save/copyfrontend/tests/setup.ts— vitest setup (fake-indexeddb, mocks, in-memory backend clipStore).agents/skills/— OpenCode skill definitions
- Frontend source is under
frontend/, notsrc/. SvelteKitfilesconfig insvelte.config.jspoints routes/lib/hooks there. - Vitest aliases:
$lib→frontend/lib,$app→frontend/app,$generated→build. Note:$app/navigationand$app/pathshave stub files infrontend/app/(navigation.ts,paths.ts) specifically for test resolution. - Svelte 5 runes: Components use
$state,$derived,$effect,$bindable,$props. However, some global state (modals, clip state) still uses Sveltewritablestores fromfrontend/lib/api/store.ts. - Tests use
jsdom(nothappy-dom). Many browser APIs are mocked infrontend/tests/setup.ts:indexedDB(fake-indexeddb),prismjs,qrcode,navigator.clipboard,navigator.mediaDevices,matchMedia,ResizeObserver,Element.prototype.animate/getAnimations,HTMLCanvasElement.getContext,ClipboardEvent. - DESIGN.md.bak describes aspirational features. Trust the actual
backend/main.moandfrontend/code overDESIGN.md.bak/PRD.md. - Receiving clips:
/list→ "Receive" generates a receiving clip (ID + password in URL hash). Another device scans QR or visits/send?clipId#password, pastes text, and the receiver'sGridViewpolls the backend viafetchClip+decrypt, then updates the local clip text and clearsreceivingflag. Polling is chainedsetTimeout-based, not a fixed interval. - Scratchpad / dirty clips: Clips edited in
GridViewbut not yet saved to IndexedDB are tracked via theeditsSvelteSet.isDirty(id)checks the local-store dirty set. - GridView URL sync:
focusClipandfocusMaximizedare synced to URL params (?clip=ID&max=1) viagoto(..., { replaceState: true, noScroll: true, keepFocus: true }). - Server hook:
frontend/hooks.server.tsredirects legacy/?clipIdURLs to/view?clip=clipId(302). - Dev proxy:
vite.config.jsproxies/apitohttp://backend.local.localhost:8000for local replica calls. test.moandbackend/test.wasmexist but are experimental/standalone; do not confuse withfrontend/tests/.- CI: GitHub Actions runs in Nix (
nix-build). Cachix cachekopicat. See.github/workflows/ci.yml. - Nix environment:
default.nix+shell.nixdefine the dev shell (Motoko compiler, vessel, didc, Node.js, pnpm, ImageMagick).
This project is licensed under the MIT License. See LICENSE for details.
load skills from .agents/skills/ for domain-specific guidance:
- motoko — Motoko pitfalls, stable types, mo:core standard library
- stable-memory — persisting state across canister upgrades
- certified-variables — certified data API, certificate validation
- canhelp — querying canister interfaces by canister ID