Help us improve
Share bugs, ideas, or general feedback.
From assembly
Assembly governance application development with Go, Templ, and Datastar. Use when building pages, adding handlers, creating Templ templates, writing database queries, scaffolding CRUD flows, configuring Docker, deploying to production, or working on any Assembly feature. Also use when asking about page types, DTO patterns, component library, Datastar integration, migration files, or module architecture. Covers pages, components, handlers, database patterns, setup, and deployment across all project phases.
npx claudepluginhub design-machines-studio/depot --plugin assemblyHow this skill is triggered — by the user, by Claude, or both
Slash command
/assembly:developmentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
!`docker compose ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null || echo "Docker not running"`
Provides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Prepares production launches with a pre-launch checklist, monitoring setup, staged rollout planning, rollback strategies, and feature flag guidance.
Share bugs, ideas, or general feedback.
!docker compose ps --format "table {{.Name}}\t{{.Status}}" 2>/dev/null || echo "Docker not running"
Build cooperative governance applications with Go, Templ, and Datastar. Build pages fast, review often, commit frequently.
Prototype toward production. Every page you build is real code. Use mockup data in SQLite, but structure your handlers and DTOs for real queries. The prototype becomes the product.
Optional companion plugins:
This skill focuses on the Assembly workflow for building pages and features.
NEVER run Go commands on the host. All Go/Templ commands run in Docker:
# CORRECT
docker compose exec app templ generate
docker compose exec app go build ./cmd/api
docker compose exec app go test ./...
# WRONG - Never do this
templ generate
go build ./cmd/api
Assembly customizes its appearance through project-level CSS overrides, NOT by editing the Live Wires framework.
Live Wires (livewires/) is a shared CSS framework. Assembly consumes it as a dependency. When you need to adjust a Live Wires component's appearance for Assembly:
src/css/6_components/ directorylivewires/ repo -- not livewires/src/css/, not component files, not token files.card--stat for Assembly, create or edit src/css/6_components/_card-overrides.css, not livewires/src/css/6_components/_card.cssThis follows the Live Wires philosophy: "Start with Live Wires, make it your own." Each project customizes in its own CSS layer.
Think in page types, not individual pages. One template serves many instances.
/members/ → index.templ (list)
/members/{id} → show.templ (detail)
/members/new → new.templ (create form)
/governance/proposals/ → index.templ, show.templ, new.templ
Define data shapes in the fixture's model/ directory (e.g., internal/fixtures/governance/model/proposal.go):
package model
type ProposalResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
ProposedBy *string `json:"proposed_by,omitempty"`
// Use pointers for optional fields
}
Handlers fetch data and render templates. Keep them thin:
func (h *Handlers) PageProposals(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
proposals, err := h.fetchProposals()
if err != nil {
log.Printf("Error fetching proposals: %v", err)
proposals = []dto.ProposalResponse{}
}
proposalsPage.Index(proposals).Render(r.Context(), w)
}
Templates live in backend/internal/pages/{domain}/:
templ Index(proposals []dto.ProposalResponse) {
@layouts.Sidebar(layouts.PageMeta{
Title: "Proposals",
BodyClass: "pg-proposals",
}) {
@partials.NavGovernance()
<article class="content">
// Page content using dto data
</article>
}
}
In backend/cmd/api/main.go:
r.Get("/governance/proposals", h.PageProposals)
r.Get("/governance/proposals/{id}", h.PageProposalDetail)
docker compose exec app templ generate
docker compose exec app go build ./cmd/api
docker compose restart app
curl http://assembly.coop.site/governance/proposals
Check backend/internal/components/ before creating anything new:
| Component | Usage |
|---|---|
components.Avatar(name, size, imageURL) | Member avatars (circle only, name rendered by caller) |
components.Badge(text, variant) | Status badges |
components.StatusBadge(status) | Governance status |
components.ButtonLink(href, text, variant) | Action buttons |
components.FormField(...) | Form inputs |
components.StatCard(...) | Dashboard metrics |
If a component needs new functionality:
// Before: Badge only took text
templ Badge(text string)
// After: Badge takes optional variant
templ Badge(text string, variant string)
Ask: "Will this be used in 3+ places?" If yes, create a component. Otherwise, inline it.
Seed realistic data in backend/migrations/:
-- 010_seed_proposals.sql
INSERT INTO proposals (id, title, status, proposed_by) VALUES
('prop-001', 'Professional Development Fund', 'voting', 'member-001'),
('prop-002', 'Office Lease Renewal', 'discussion', 'member-002');
List queries - return slices:
func (h *Handlers) fetchProposals() ([]dto.ProposalResponse, error) {
rows, err := h.db.Query(`SELECT id, title, status FROM proposals`)
// ...
}
Detail queries - return single item:
func (h *Handlers) getProposal(id string) (*dto.ProposalResponse, error) {
row := h.db.QueryRow(`SELECT id, title, status FROM proposals WHERE id = ?`, id)
// ...
}
Prefer static SQL with optional predicates over dynamic fmt.Sprintf query building. Dynamic SQL triggers gosec G202 and risks injection.
// CORRECT — static SQL with conditional append
query := `SELECT id, title, status FROM $TABLE WHERE 1=1`
args := []any{}
if status != "" {
query += ` AND status = ?`
args = append(args, status)
}
if memberID != "" {
query += ` AND proposed_by = ?`
args = append(args, memberID)
}
rows, err := db.QueryContext(ctx, query, args...)
// WRONG — dynamic SQL via fmt.Sprintf (gosec G202)
query := fmt.Sprintf(`SELECT * FROM %s WHERE status = '%s'`, table, status)
Batch fetch related data:
// 1. Fetch all meetings
meetings, ids := fetchMeetings()
// 2. Batch fetch related resolutions
resolutions := fetchResolutionsByMeetingIDs(ids)
// 3. Group in memory
resByMeeting := groupBy(resolutions, "meeting_id")
Define signals at the article level for filtering:
<article class="content" data-signals="{ statusFilter: 'all', yearFilter: '2026' }">
Use data-class for active states:
<button type="button" class="button button--small"
data-class:button--accent="$statusFilter === 'all'"
data-on:click="$statusFilter = 'all'">All</button>
Generate filter expressions for each row:
// SAFETY: p.Status is a controlled enum from the database (draft, active, closed),
// not user-supplied text. Do NOT interpolate arbitrary user input into expressions.
func rowFilter(p dto.ProposalResponse) string {
status := strings.ToLower(p.Status)
return "($statusFilter === 'all' || $statusFilter === '" + status + "')"
}
<tr data-show={ rowFilter(p) }>
Use explicit string comparisons ($filter === 'all') when CSS classes or visibility depend on exact values. Boolean signals ($showDrafts) work for simple show/hide but break when data-class needs to match one of several states. When in doubt, use string signals with === matching.
// CORRECT — string signal with exact matching
data-signals="{ view: 'grid' }"
data-class:active="$view === 'grid'"
// RISKY — boolean loses which state is active
data-signals="{ isGrid: true }"
data-class:active="$isGrid" // Can't distinguish grid vs list vs table
The appctx package (backend/internal/appctx/modules.go) provides request-scoped values shared across packages. It breaks import cycles between handlers (which set values) and templates (which read them).
type NavUser struct {
MemberID string
Initials string
Name string
Status string // "active", "probationary", "departed"
IsBoard bool
IsOfficer bool
IsAdmin bool
ProfileIncomplete bool // true when phone == "" || bio == ""
}
| Function | Purpose |
|---|---|
SetNavUser(ctx, user) / GetNavUser(ctx) | Store/retrieve member identity. GetNavUser returns safe fallback (Initials: "?") if not set. |
SetEnabledModules(ctx, map) / IsModuleEnabled(ctx, slug) | Module gating. IsModuleEnabled is fail-closed (returns false if no context). |
SetNavPath(ctx, path) / GetNavPath(ctx) | Active nav state for sidebar highlighting. |
SetNavPrefs(ctx, closedGroups) / GetNavPrefs(ctx) | Collapsed nav groups from nav_closed cookie. |
AppContextMiddleware in backend/internal/handlers/middleware.go runs on every request:
db.EnabledModuleSlugs())nav_closed cookie for collapsed nav groupscoop_member cookie for current member ID (via resolveNavMemberID)preferred_name, phone, bio, status, is_super_admin)member_roles for board and officer statusSetEnabledModules, SetNavPath, SetNavPrefs, SetNavUser// Read the current user in any template
user := appctx.GetNavUser(ctx)
if user.IsAdmin {
// render admin controls
}
// Check module availability before rendering nav items
if appctx.IsModuleEnabled(ctx, "governance") {
// render governance nav section
}
During development, switch the active member identity to test different personas and permission levels without modifying code or database.
How it works: getCurrentMemberID() in backend/internal/handlers/handlers.go checks three sources in order:
?member= query parameter (development only, gated by ENV=development). Validates the member exists in the database, then sets the coop_member cookie for 7 days.coop_member cookie (persists across requests, HttpOnly, SameSite=Lax).mem_009 (Ned Ludd) if neither source has a value.Prototype only: This entire fallback chain is a prototype convenience. In production, RequireAuth middleware fires before any handler, so getCurrentMemberID() is never reached without a valid session. The ?member= parameter and mem_009 default are unreachable in production builds (gated by ENV=development).
Usage: Append ?member=mem_001 to any URL. The cookie persists, so subsequent requests use that identity automatically.
Persona mapping for testing:
| Persona | Member ID | Role |
|---|---|---|
| David (casual member) | Use mem_XXX from seed data | Active member, no board role |
| Aisha (reluctant board member) | Use board member seed ID | Board role, mobile-first |
| Alex (new probationary) | Use probationary seed ID | Status: "probationary" |
Security note: This is UI-level identity only, NOT a security boundary. The coop_member cookie is unauthenticated and forgeable. Real authentication is required before production. The ?member= param is environment-gated to ENV=development only.
Beacon for the pipeline's Fixture Discovery step (see plugins/pipeline/skills/assess/SKILL.md). Canonical persona list:
| Member ID | Persona | Use for |
|---|---|---|
mem_001 | Director | privileged actions, approvals |
mem_005 | Aisha -- member without position | empty-state views, unprivileged flows |
mem_009 | Ned Ludd (default) | baseline sanity |
mem_012 | David -- authored content | authored-content views |
Consult internal/fixtures/<module>/seed.go for the full seeded member list.
Generate and build:
docker compose exec app templ generate
docker compose exec app go build ./cmd/api
Test in browser:
curl http://assembly.coop.site/{your-route}
open http://assembly.coop.site/{your-route}
Simplify:
Run /simplify on the files you just changed. This catches complexity creep, dead code, redundant abstractions, and over-engineering before they compound. If /simplify makes changes, rebuild and retest.
Commit and push:
git add -A
git commit -m "feat: Add {feature} page"
git push
Each fixture owns its code in internal/fixtures/{name}/:
internal/fixtures/governance/
├── routes.go # Chi route registration
├── handlers.go # Thin HTTP adapters (parse → call service → render)
├── services/ # Business logic
│ ├── proposals.go
│ └── meetings.go
├── model/ # DTOs and domain types (fixture-owned)
│ ├── proposal.go
│ └── meeting.go
└── pages/ # Templ page templates (fixture-owned)
├── proposals/
│ ├── index.templ
│ └── show.templ
└── meetings/
└── index.templ
Baseplate code (members, admin, auth, groups) lives in internal/baseplate/.
Shared components in internal/components/ accept primitive props only (strings, ints, bools). They never import fixture-specific DTOs or model packages. If a component needs fixture data, the caller maps it to primitives before passing.
// CORRECT — primitive props
components.Avatar(member.Name, "md", member.ImageURL)
// WRONG — importing fixture model
components.ProposalCard(governance.Proposal{...})
Every state-changing operation (create, update, delete, status transition) must follow this sequence. Skipping a step is a review finding.
deps.Auth.Authorize(ctx, "entity.action", resource) before any write. Route middleware (RBAC) is not sufficient; object-level auth is required for every mutation.db.WithTx(). Number generation (sequence IDs, resolution numbers) happens inside the transaction.deps.Audit inside the transaction. Include actor, action, entity, and changed fields.deps.Events.Publish() only AFTER tx.Commit() returns nil. Never publish inside the transaction scope.Produce this map before review on any PR touching authentication, authorization, admin surfaces, member mutations, install flow, account/profile state, fixture enablement, or UI capability flags.
Middleware is a route precondition, not an authorization decision. Every mutation and every privileged read (member PII, financial data, audit logs) requires an explicit deps.Auth.Authorize(ctx, action, resource) call in the service layer. The boundary map makes this visible.
RequireAuth, RequireAdmin, RequireSuperAdmin, RequirePermission, RequireModule).LoadMember, install guard, session rotation, or account existence handling.Fill one row per route, handler action, service mutation, or privileged UI capability:
| Surface | File / Handler | Method + Route | Actor | Route Middleware | Authorizer Action | Resource | Read / Write Target | UI Capability Source | Failure Mode | Tests |
|---|---|---|---|---|---|---|---|---|---|---|
| example | internal/baseplate/admin/... | POST /admin/... | active admin | RequireAuth -> LoadMember -> RequireAdmin | admin.*.update | baseplate admin resource with object ID | static SQL service mutation in tx | deps.Auth.Authorize, default-deny on error | 403 before mutation, no event/audit | focused handler + service test |
ContextMember -- handlers must handle auth.ContextMember(ctx) returning nil without panic or accidental privilege. A nil check before any .ID or .Role access is mandatory.Authorize(ctx, action, resource) pairs as the handler. Default deny on nil authz, missing member, unexpected account type, or authorization error.tx.Commit() (per Mutation Invariant #6). The map should note which events each surface publishes and confirm none are inside the transaction.For each mapped row, include at least one focused test for the failure mode:
Add this receipt to the PR body or final cross-check:
Auth Boundary Map:
- Mapped surfaces:
- Middleware checked:
- Authorizer action/resource pairs checked:
- Default-deny UI capabilities checked:
- Stale-session/operator/install edge cases checked:
- Tests added or verified:
- Residual risk:
For detailed reference:
Every page should be a Templ template with database data:
<!-- WRONG: Static HTML in public/ -->
<h1>John Smith</h1>
<!-- RIGHT: Dynamic Templ template -->
<h1>{ member.FullName }</h1>
Data comes from handlers:
// WRONG
templ Show() {
<h1>January Board Meeting</h1>
}
// RIGHT
templ Show(meeting dto.MeetingResponse) {
<h1>{ meeting.Title }</h1>
}
Every major chunk gets reviewed. The review catches issues early.
Every user-facing feature gets tested through the UX persona framework at tests/ux/. This isn't optional -- it catches issues that code review misses (jargon barriers, permission confusion, mobile dead ends).
Use pointers for optional fields and check them:
if m.ChairName != nil {
<dt>Chair</dt>
<dd>{ *m.ChairName }</dd>
}
# Generate Templ files
docker compose exec app templ generate
# Build the Go binary
docker compose exec app go build ./cmd/api
# Restart to pick up changes
docker compose restart app
# Check a page works
curl http://assembly.coop.site/governance/proposals
# Run tests
docker compose exec app go test ./...
# View the app
open http://assembly.coop.site
Assembly uses a Baseplate + Fixtures architecture.
Module interface, register via init(), included via blank importsgov_, doc_, disc_), not separate databasesDo:
gov_, doc_, disc_)entity_references for cross-module relationshipsmodules.enabled before rendering nav itemsDon't:
config.Config structmodels/ packageAssembly follows a three-phase distribution model. See docs/DISTRIBUTION.md for the full specification and docs/PILOT-SCOPE.md for what ships first.
The production backend lives in a separate repo (assembly-baseplate, DM-021) and is built from first principles. The prototype (assembly, DM-006) is the design workspace — patterns are validated there, then implemented properly in production. Neither blocks the other. See ADR-002.
| Repo | Project | Purpose |
|---|---|---|
assembly/ | DM-006 | Prototype, UI/UX design, persona testing, mockup data |
assembly-baseplate/ | DM-021 | Production platform, auth, NATS, SQLite, install, CLI |
Design in the prototype → extract validated pattern → implement in production.
Fixtures implement the Module interface for clean isolation (Caddy-style compile-time registration):
type Module interface {
ID() string // "governance"
Name() string // "Governance"
SetupRoutes(r chi.Router, deps *app.Dependencies) error // Mount routes
Migrations() embed.FS // Embedded SQL migrations
}
// Optional: declare NATS event patterns
type EventDeclarer interface {
Events() module.EventConfig
}
Fixtures register via init() and are included via blank imports in cmd/api/imports.go:
import _ "github.com/Design-Machines-Studio/assembly-governance"
Each fixture receives a Dependencies struct via SetupRoutes():
type Dependencies struct {
DB *ScopedDB // Restricted to fixture's tables
Auth *Authorizer // Object-level authorization
Members MemberReader // Read-only member lookups
Events ScopedEventBus // Restricted NATS subjects
Audit AuditWriter // Shared audit log access
Config ConfigReader // Co-op settings
Logger *slog.Logger
}
Wraps *sql.DB with table-prefix enforcement. Fixtures never bypass ScopedDB — this is the core data isolation contract.
members, groups, permissions, audit_log)gov_proposals, doc_documents, eq_shares, health_metrics)Fixtures use $TABLE and $PREFIX_ placeholders in queries — ScopedDB substitutes the correct prefix at runtime. See ADR-003 (in the assembly-baseplate repo at docs/adr/).
Each fixture gets a scoped event bus restricting NATS publishing to its subject prefix:
type ScopedEventBus struct {
bus EventBus
prefix string // "assembly.gov."
allowRead []string // Cross-boundary read subjects
}
allowRead permits subscribing to specific cross-boundary subjects (e.g., governance subscribing to assembly.member.status_changed). See the nats-jetstream skill for full patterns.
Note: ADRs (ADR-002 through ADR-007) live in the assembly-baseplate repo at docs/adr/, not in the depot.
Handlers → Services → ScopedDB. Services contain business logic. Handlers are thin HTTP adapters (parse request, call service, render response).
Size limits: No handler file over 200 lines. No service file over 500 lines. Split into focused files if needed.
// Handler (thin adapter)
func (h *Handlers) ListProposals(w http.ResponseWriter, r *http.Request) {
proposals, err := h.service.ListProposals(r.Context())
if err != nil {
http.Error(w, "Internal error", http.StatusInternalServerError)
return
}
pages.Index(proposals).Render(r.Context(), w)
}
// Service (business logic)
func (s *GovernanceService) ListProposals(ctx context.Context) ([]Proposal, error) {
return s.db.Query("gov_proposals", "SELECT * FROM $TABLE ORDER BY created_at DESC")
}
Three-layer authorization (ADR-004):
Layer 1 — Route middleware:
RequireAuth — session existsRequirePermission("governance.view") — role-basedRequireModule("governance") — fixture enabledRequireAdmin — board/officer/super_adminLayer 2 — Object-level (core):
func (a *Authorizer) Authorize(ctx context.Context, action string, resource Resource) error
Default-deny switch on action strings (proposal.edit, meeting.manage, vote.cast). The Resource struct carries ID, AuthorID, Status, GroupID for ownership/visibility checks.
Layer 3 — Template conditional rendering: Delegates to Authorize() internally. UX concern, not a security boundary.
Each install generates a UUID (install_id) and Ed25519 keypair at first boot (ADR-005). The keypair lives at data/identity.key (0600 permissions).
Well-known endpoint:
GET /.well-known/assembly
{
"install_id": "uuid",
"name": "TACO",
"protocol_version": 1,
"public_key": "base64-ed25519-public-key",
"federation": true
}
Federation uses OAuth-style account linking with signed tokens: 5-minute TTL, single-use nonce, audience validation, HTTPS required in production. See ADR-006 for the full linking flow.
spf13/cobra provides the CLI:
| Command | Purpose |
|---|---|
assembly serve | Start HTTP server (default) |
assembly admin create | Headless install (--name, --email, --password-file) |
assembly admin reset-password | CLI password reset (--email) |
assembly migrate | Run pending goose migrations |
assembly seed --demo | Load Catalyst Cooperative demo data (dev only) |
assembly version | Show version and install ID |
assembly backup | Manual SQLite backup |
Passwords only via --password-file or --password-stdin — never env vars or CLI flags. See ADR-005.
Same Docker-only rule as the prototype:
docker compose exec app go test -race -cover ./...
docker compose exec app go build ./cmd/api
docker compose exec app templ generate
Assembly has a persona-based UX test framework at tests/ux/. Use it when building or reviewing user-facing features.
Six personas mapped to real seed accounts, covering the full spectrum of co-op member engagement:
| Persona | Account | Role | Key Testing Focus |
|---|---|---|---|
| Reluctant Board Member | mem_005 | Board, low engagement | Mobile usability, anxiety, dead ends |
| Power Secretary | mem_003 | Officer, daily user | Efficiency friction, workflow bottlenecks |
| Engaged Chair | mem_001 | Chair + admin | Admin/governance boundary, dual-role confusion |
| Casual Member | mem_007 | Regular member | "Just let me do the thing" friction, jargon barriers |
| New Probationary | mem_010 | Probationary | Permission boundaries, onboarding gaps |
| Numbers Treasurer | mem_002 | Officer, detail-focused | Compliance visibility, data accuracy |
Read the full persona files at tests/ux/personas/ for backstories, behavioral patterns, patience thresholds, and abandonment triggers.
Two heuristic checklists at tests/ux/heuristics/:
tests/ux/coverage-matrix.md) to see which personas and tasks cover the area you're working ontests/ux/tasks/{area}/ following the frontmatter format in the READMEWrite a task file when you add:
| Skill | Plugin | When to Load |
|---|---|---|
| nats-jetstream | assembly | Embedded NATS patterns, KV store, event bus, SSE streaming |
| golang-patterns | assembly | Go library choices (SQLite, sessions, CSRF, migrations, CLI) |
| governance | council | Co-op domain knowledge, voting thresholds, compliance requirements |
| decolonial-language | council | UI labels, member-facing copy, terminology mappings |
| strategy | design-machines | Product positioning, pricing, client pipeline context |
| typography | design-practice | Type scale, baseline rhythm, Live Wires alignment |
Official and third-party Claude Code plugins that complement this skill:
| Plugin | Tool | When to Use |
|---|---|---|
| compound-engineering | go-build-verifier, css-reviewer, security-sentinel agents | Go build verification, CSS compliance, security review |
| context7 | /context7 | Live documentation lookup for Go stdlib, Templ, Datastar |
| playwright | Browser tools | E2E visual testing beyond curl smoke tests |
| superpowers | /debug, /verify | Debug tricky Go issues, verify builds |
| feature-dev | /feature-dev | Structured feature development with architecture exploration |
| userback | Feedback tools (MCP) | Triage user feedback, read console/network logs, update status. HTTP endpoint: https://mcp.userback.io/v1/mcp/ (OAuth) |
decolonial-language skill): For values-aligned terminology when naming components, writing UI labels, seeding mock data, and writing microcopy. Provides the three-layer architecture (legal → bridge → cultural) for mapping BC Act terms to solidarity economy language. Default to cultural layer in member-facing templates; use legal layer only in generated compliance documents.docs/DISTRIBUTION.md (deployment model), docs/PILOT-SCOPE.md (pilot checklist), docs/UPDATE-FLOW.md (update sequence)