Note (2026-04-24): After this document was written, legal_entity was renamed to tenant and the old tenant was renamed to organization. Read references to these terms with the pre-rename meaning.

File Store Admin UI — Design

Summary

Add an admin page to configure the FP&A file store for a legal entity. The UI lets the admin select a file store type, enter backend-specific config, validate and test the connection on save, and re-test or remove existing configurations.

Architecture

Discovery via multimethod

A new file-store-admin multimethod in com.getorcha.link.mcp.file-store dispatches on the protocol string (same dispatch as make-file-store). Each backend that wants admin UI support registers a method returning a descriptor map:

(defmulti file-store-admin
  "Returns admin descriptor for a file store type.
  Only backends that register here appear in the admin UI."
  identity)

Descriptor shape:

{:display-name "Local Filesystem"
 :form-schema  [:map
                [:path [:string {:min 1
                                 :form/label "Directory Path"
                                 :form/placeholder "/data/acme-gmbh/"
                                 :error/message "Directory path is required"}]]]}

The admin discovers available types via (keys (methods file-store/file-store-admin)). Adding a new backend = register both make-file-store and file-store-admin, it appears automatically.

Schema-driven forms

The :form-schema is a flat Malli :map schema with :form/* properties on each field. A generic renderer walks m/children, dispatches on m/type, and emits hiccup form fields. Supported types:

The renderer accepts optional values (pre-fill) and errors (me/humanize output) maps.

Validation & coercion

Malli handles the full pipeline from a single schema:

  1. m/decode with mt/string-transformer + mt/strip-extra-keys-transformer — coerces string form params to typed values, strips extra keys
  2. m/explain + me/humanize — per-field error messages back to the form

No per-backend coercion function needed for now. The decoded map gets {"protocol" protocol-key} merged in before storage.

Connection testing on save

After schema validation passes, the save handler:

  1. Merges {"protocol" ...} into the decoded config
  2. Calls make-file-store — catches construction errors (e.g., "not a directory")
  3. Calls list-files on root — catches IO errors
  4. If all passes → stores JSONB in legal_entity.fpna_data_source
  5. If any step fails → returns the error to the form

Routes

Namespace: com.getorcha.admin.http.tenants.file-store

Base path: /tenants/-/legal-entities/:le-id/file-store

Method Path Purpose
GET / Render page (empty form or pre-filled)
POST / Save config (decode → validate → test → store)
POST /test Test existing saved config on demand
DELETE / Remove config (set fpna_data_source to NULL)

HTMX interactions

UI States

No config

Configured

Add a folder icon link on the legal entity row pointing to /tenants/-/legal-entities/:le-id/file-store, next to the existing prompts link.

File Store: Local Filesystem

The only backend for now. Registers both multimethods:

;; Already exists:
(defmethod file-store/make-file-store "file" [config] ...)

;; New:
(defmethod file-store/file-store-admin "file" [_]
  {:display-name "Local Filesystem"
   :form-schema  [:map
                  [:path [:string {:min 1
                                   :form/label "Directory Path"
                                   :form/placeholder "/data/acme-gmbh/"
                                   :error/message "Directory path is required"}]]]})

Data Model

No schema changes needed. Uses existing legal_entity.fpna_data_source JSONB column (added in migration 20260304215718).

Stored format: {"protocol": "file", "path": "/data/acme-gmbh/"}