Files
penpot/backend/AGENTS.md
2026-03-09 16:23:28 +01:00

2.7 KiB
Raw Blame History

backend Agent Instructions

Clojure service running on the JVM. Uses Integrant for dependency injection, PostgreSQL for storage, and Redis for messaging/caching.

Commands

# REPL (primary dev workflow)
./scripts/repl              # Start nREPL + load dev/user.clj utilities

# Tests (Kaocha)
clojure -M:dev:test                                         # Full suite
clojure -M:dev:test --focus backend-tests.my-ns-test        # Single namespace

# Lint / Format
pnpm run lint:clj
pnpm run fmt:clj

Test namespaces match .*-test$ under test/. Config is in tests.edn.

Integrant System

src/app/main.clj declares the system map. Each key is a component; values are config maps with ::ig/ref for dependencies. Components implement ig/init-key / ig/halt-key!.

From the REPL (dev/user.clj is auto-loaded):

(start!)   ; boot the system
(stop!)    ; halt the system
(restart!) ; stop + reload namespaces + start

RPC Commands

All API calls: POST /api/rpc/command/<cmd-name>.

(sv/defmethod ::my-command
  {::rpc/auth true             ;; requires authentication (default)
   ::doc/added "1.18"
   ::sm/params [:map ...]      ;; malli input schema
   ::sm/result [:map ...]}     ;; malli output schema
  [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
  ;; return a plain map; throw via ex/raise for errors
  {:id (uuid/next)})

Add new commands in src/app/rpc/commands/.

Database

app.db wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.

;; Query helpers
(db/get pool :table {:id id})                    ; fetch one row (throws if missing)
(db/get* pool :table {:id id})                   ; fetch one row (returns nil)
(db/query pool :table {:team-id team-id})        ; fetch multiple rows
(db/insert! pool :table {:name "x" :team-id id}) ; insert
(db/update! pool :table {:name "y"} {:id id})    ; update
(db/delete! pool :table {:id id})                ; delete
;; Transactions
(db/tx-run cfg (fn [{:keys [::db/conn]}]
                 (db/insert! conn :table row)))

Almost all methods on app.db namespace accepts pool, conn or cfg as params.

Migrations live in src/app/migrations/ as numbered SQL files. They run automatically on startup.

Error Handling

(ex/raise :type :not-found
          :code :object-not-found
          :hint "File does not exist"
          :context {:id file-id})

Common types: :not-found, :validation, :authorization, :conflict, :internal.

Configuration

src/app/config.clj reads PENPOT_* environment variables, validated with Malli. Access anywhere via (cf/get :smtp-host). Feature flags: (cf/flags :enable-smtp).