* feat(attachments): CLI + TypeScript clients + types (TASK-873)
Rounds out the API surface with Go and TS client methods + a
\`pad attachment\` Cobra subcommand for ops debugging.
internal/cli/client.go
AttachmentUploadResult struct mirrors POST /attachments JSON.
UploadAttachment streams a multipart file part via io.Pipe — never
buffers the upload in memory. itemRef is optional. Uses a fresh
http.Client with a 5-minute timeout per request so a 25 MiB upload
over a constrained link doesn't trip the package-shared 10s default.
DownloadAttachment streams the bytes into the caller's writer,
returning Content-Type + total bytes copied. Optional ?variant=
parameter for thumbnails (server falls back to original silently
per TASK-872).
cmd/pad/main.go
pad attachment upload <item-ref|-> <path> [--filename NAME]
pad attachment download <id> <out|-> [--variant thumb-sm|thumb-md]
Item arg accepts an issue ref (TASK-5) or slug; "-" means no parent.
Out arg "-" streams to stdout (with status messages on stderr) so
callers can pipe into image viewers etc. Resolves the item via
GetItem first so a typo'd ref fails fast with a useful error.
List + delete subcommands intentionally omitted — those endpoints
ship with TASK-881 (storage usage) and the future GC task. Adding
client methods that hit 404s would mislead callers; same logic kept
the upload response's "url" out of TASK-871 until TASK-872 wired GET.
web/src/lib/types/index.ts
Attachment interface mirroring the Go model (pointer types → optional).
AttachmentUploadResult interface for the upload response shape.
web/src/lib/api/client.ts
api.attachments.upload(workspaceSlug, file, itemId?) — multipart
POST via direct fetch (skips shared request() because that helper
hard-codes Content-Type: application/json). Carries CSRF, cookies,
and the same 401 → /login redirect.
api.attachments.downloadUrl(workspaceSlug, attachmentId, variant?)
is a pure URL builder so callers can wire <img src> directly without
going through fetch.
End-to-end smoke verified:
pad attachment upload TASK-869 /tmp/tiny.png # uploads PNG
pad attachment download <id> /tmp/dl.png # bytes are identical
cmp /tmp/tiny.png /tmp/dl.png # PASS
Verification
go build ./... — clean
go vet ./... — clean
go test ./... — all packages pass
cd web && npm run build — clean
make install — server restarts on the new binary
Parent: PLAN-866.
* fix(cli): atomic download — write to temp + rename so a failed download doesn't truncate the destination per Codex review (round 1)
P2: pad attachment download <bad-id> /existing/file used to wipe the
existing file on auth/network/404 errors because os.Create truncated
before the request was even attempted. The bytes were never written
because DownloadAttachment errored out, but the destination was
already 0 bytes — a footgun for anyone running the CLI in scripts.
Fix: for the file-path case, write to a sibling .tmp via os.CreateTemp
in the destination directory, fsync, close, then os.Rename only on
success. Same atomic-write pattern as FSStore.Put. The defer cleans
up the .tmp on any error path.
The stdout case (outPath == "-") is unchanged — bytes already
streamed to stdout can't be rolled back, so any partial write is
just visible to the caller as a short payload.
Verified end-to-end:
echo X > /tmp/existing.png
pad attachment download not-a-real-id /tmp/existing.png # errors
cat /tmp/existing.png # still "X" — file untouched
* docs(cli): clarify os.Rename atomic-replace behavior on Windows (Codex round 2 disagreement)
Round 2 flagged this as P2: "os.Rename does not replace an existing
destination on Windows." That is technically incorrect for modern Go.
Verified directly against the Go stdlib source:
src/internal/syscall/windows/syscall_windows.go:
func Rename(oldpath, newpath string) error {
...
return MoveFileEx(from, to, MOVEFILE_REPLACE_EXISTING)
}
MoveFileEx with MOVEFILE_REPLACE_EXISTING atomically replaces an
existing destination on Windows. This has been the behavior since
Go 1.5 (2015), so every version of Go this codebase supports already
gets the desired replace-on-rename semantics on every platform.
Added an inline code comment so future readers don't worry about the
same false alarm. No code-path change.
Migrates from xarmian/pad to PerpetualSoftware/pad across the entire
repo and updates the product subtitle to "Collaborate with your AI
agents".
Go module rename
- go.mod: github.com/xarmian/pad → github.com/PerpetualSoftware/pad
- All Go imports updated across cmd/pad, internal/{cli,server,store,
models,collections,items,events,metrics,webhooks} (~130 files)
- Test fixtures with the literal repo slug ("xarmian/pad" in JSON
shapes, SSH/HTTPS git URL strings, workspace_context fixtures)
also updated, including the secondary repo entry
(xarmian/pad-web → PerpetualSoftware/pad-web — pad-web was also
moved to the org per branch context)
Docs / config
- README badges, install instructions, brew tap, Docker image, source
build path, sponsor link (sponsor link kept as personal @xarmian)
- Subtitle: "Project management for developers and AI agents." →
"Collaborate with your AI agents." (README, manifests, web layout
meta, .goreleaser homebrew description)
- CONTRIBUTING.md, SECURITY.md, skills/INSTALL.md
- .goreleaser.yaml: homebrew_casks owner, GHCR image, release github
owner, cosign cert-identity regex, comments
- .github/workflows/release.yml: tap/release comments
- deploy/k8s/deployment.yaml: container image
- docs/deployment.md: clone URL
- web/static/{site.webmanifest,manifest.json}: description
- web/src/routes/+layout.svelte: meta description + og:description
Brew tap path is PerpetualSoftware/tap/pad (CamelCase, matches
GitHub user case). GHCR image is ghcr.io/perpetualsoftware/pad
(lowercased per GHCR's URL normalization). CODEOWNERS @xarmian and
FUNDING.yml github: xarmian intentionally retained — those are the
personal maintainer / sponsor account, separate from the org repo.
Verification: go build ./..., go test ./... (all pkgs pass), web
build, and make install all clean (TASK-844, TASK-845).
Add star/unstar/starred CLI commands (PLAN-564, TASK-570):
- pad item star <ref> — star an item
- pad item unstar <ref> — unstar an item
- pad item starred [--all] [--format json] — list starred items
Client methods: StarItem, UnstarItem, ListStarredItems.
Replace the email/password terminal prompt in `pad auth login` with a
browser-based auth flow. The CLI creates a pending session, prints a URL
the user opens in their browser (works for localhost, remote VPS, or
Pad Cloud), and polls until the session is approved.
- Add CLI auth session endpoints (create, poll, approve)
- Add browser approval page at /auth/cli/{code}
- Rewrite `pad auth login` to use browser flow by default
- Keep `pad auth login --interactive` as email/password fallback
- Add login page redirect param support for post-login bounce-back
- Add SQLite and PostgreSQL migrations for cli_auth_sessions table
Closes PLAN-539, IDEA-404
HIGH fixes:
- Login-verify no longer accepts bare user_id. Now requires an
HMAC-signed, IP-bound, 5-minute challenge token issued during login
(prevents password bypass via known user ID + TOTP code)
- Recovery codes are SHA-256 hashed before storage; plaintext is
returned to the user once and never persisted
MEDIUM fixes:
- ConsumeRecoveryCode uses a DB transaction to prevent concurrent
double-consumption of the same recovery code
- EnableTOTP is atomic: WHERE clause requires totp_secret match to
prevent TOCTOU race between setup and verify calls
- /auth/2fa/login-verify now uses the strict Auth rate limiter
(5 req/min/IP) instead of the general API limiter
- CLI login detects requires_2fa response and prompts for TOTP code
instead of silently saving empty credentials
Extend the activities table to capture IP address and user agent for all
state-changing operations. Add audit events for auth (login, logout,
register, bootstrap, password changes), workspace management (member
invite/remove, role changes), token lifecycle, and admin settings.
- SQLite migration recreates activities table with nullable workspace_id,
new ip_address/user_agent columns, and relaxed CHECK constraints
- PostgreSQL migration adds columns and drops constraints
- New ListAuditLog store method with action/actor/workspace/date filters
- GET /api/v1/audit-log endpoint (admin-only)
- CLI: pad workspace audit-log [--days N] [--actor X] [--action X]
* feat: agent roles — role-based (user, role) assignment for items (#PHASE-9)
Introduce agent roles as a first-class concept for human-agent work
assignment. Roles describe capability specializations (Planner,
Implementer, Reviewer, etc.) and items can be assigned to a (user, role)
pair, enabling natural handoff workflows between different AI tools.
Migration:
- New `agent_roles` table (workspace-scoped, slug-unique)
- `assigned_user_id` + `agent_role_id` columns on `items` with FKs
- Removed legacy `assignee` text field from Tasks schema
Backend:
- AgentRole model + full CRUD store/API
- All item queries updated with LEFT JOINs to resolve assignment
- Item list filtering by assigned_user_id and agent_role_id
- Role transitions tracked in activity feed metadata
CLI:
- `pad role list/create/delete` commands
- `--role` and `--assign` flags on item create/update/list
- Assignment displayed in `pad item show` output
Web:
- TypeScript types + API client for agent roles
- Role badge on item cards in list/board views
- Assignment display on item detail page
* fix: enforce workspace-scoped assignments and fail fast on unresolved --assign filter
Addresses code review feedback from PR #58:
P1: Add validateAssignmentScope() to the store layer, called by both
CreateItem and UpdateItem. Verifies that assigned_user_id belongs to
the workspace (via IsWorkspaceMember) and agent_role_id exists in the
workspace (via GetAgentRole) before writing. Prevents cross-workspace
assignment leaks.
P2: The CLI `pad item list --assign <name>` now errors instead of
silently returning unfiltered results when the member lookup fails or
no workspace member matches the provided name.
* feat: add user management database migration and models
Add migration 012_users.sql with users, sessions, and workspace_members
tables. Add user_id columns to api_tokens, items, comments, activities,
item_links, and item_versions for proper user attribution. Create Go
model structs (User, Session, WorkspaceMember) in models/user.go.
* feat: add store layer for users, sessions, and workspace members
Implement CRUD operations for user management:
- users.go: create, get, update, list, validate password (bcrypt)
- sessions.go: create, validate, delete, cleanup expired (SHA-256 hashed tokens)
- workspace_members.go: add/remove members, role management, access checks
Adds golang.org/x/crypto/bcrypt dependency. Includes 16 new tests
covering all store methods, password validation, session lifecycle,
and workspace membership operations.
* feat: rewrite auth system from single-password to user-based
Replace single-password auth with email/password user authentication:
- New endpoints: POST /auth/register, GET /auth/me
- Rewritten: POST /auth/login (email+password), GET /auth/session
(needs_setup detection), POST /auth/logout (DB session destroy)
- Delete in-memory SessionManager, use DB-backed sessions via store
- New middleware: SessionAuth (cookie→user), RequireAuth (with
fresh-install passthrough when no users exist)
- Remove Password field from config, PAD_PASSWORD env var, SetPassword()
All 23 existing server tests pass (fresh DBs have no users → passthrough).
* feat: add workspace access control middleware
Add RequireWorkspaceAccess middleware that checks workspace_members for
authenticated users, with fallback for legacy API tokens and fresh
installs (no users → implicit owner). Includes role hierarchy helpers
(workspaceRole, requireRole) for downstream permission checks.
Wire middleware into the /{slug} workspace route group.
* feat: add CLI auth commands and credential storage
Add pad login, pad logout, pad whoami commands with credential
storage in ~/.pad/credentials.json (0600 permissions). Update CLI
HTTP client to auto-attach auth tokens and X-Pad-Agent header on
all requests. Add auth API methods (Login, Register, Logout,
CheckSession, GetCurrentUser). Extend .pad.toml with optional
agent_name field. Add golang.org/x/term for masked password input.
* feat: derive actor/source from auth context in all handlers
Replace hardcoded "user"/"web" actor/source strings with auth-aware
helpers. actorFromRequest() derives actor ("user"/"agent" via
X-Pad-Agent header) and source ("web"/"cli" from auth method).
agentMeta() merges agent name into activity metadata. Update all
item, document, comment, and move handlers to use request-based
logActivity/logActivityWithMeta. Remove hardcoded CreatedBy/Source
from all CLI commands — server now determines these from auth context.
* feat: frontend auth — login, registration, auth guard, user menu
Rewrite login page with email/password fields, add registration page
for first-time setup, update auth guard to handle needs_setup redirect.
Add user menu to sidebar with logout. Update API client with new auth
methods (register, login with email, session with needs_setup flag).
* feat: migrate API tokens from workspace-scoped to user-owned
API tokens now have a user_id owner and optional workspace_id scope.
CreateAPIToken takes userID as first parameter. ValidateToken resolves
the token's user into the request context. TokenAuth middleware now
sets ctxCurrentUser when a user-owned API token is used. Add user-
scoped endpoints: GET/POST/DELETE /auth/tokens. Keep workspace-scoped
token endpoints for backwards compatibility.
* feat: workspace membership, invitations, and role enforcement
Add workspace_invitations table (migration 013) with join codes.
Implement invitation store methods (create, get by code, accept,
list). Add member management handlers: list members + invitations,
invite (auto-adds existing users or creates invitation), remove
member, change role, accept invitation by code. Add API routes
under /workspaces/{slug}/members/* and /invitations/{code}/accept.
Add CLI commands: pad members, pad invite, pad join.
* feat: auth tests and documentation updates
Add comprehensive auth endpoint tests: registration flow (first user
becomes admin), login/logout, validation errors, duplicate email,
auth enforcement (401 after users exist, exempt paths), /me endpoint.
Update CLAUDE.md and README.md to document user-based auth system,
replacing old PAD_PASSWORD references with pad login/members/invite
workflow and role-based access control.
* feat: add members management UI to workspace settings page
Add Members section to settings with: member list (avatar, name,
email, role), role change dropdown (owner only), remove button
(owner only), pending invitations display with join codes, and
invite form with email + role picker. Add members API methods to
the TypeScript client (list, invite, remove, updateRole).
* fix: backfill workspace owners for pre-migration workspaces
Add backfillWorkspaceOwners() that runs on server start. For any
workspace with no members, adds the first admin user as owner.
This handles the migration case where workspaces existed before the
user system — without it, the members list shows empty.
* feat: shareable invite links with /join/[code] page
Replace raw join codes with full shareable URLs. Server generates
join_url using its configured base URL (e.g. https://pad.example.com/
join/a3f8b2c1). New /join/[code] page handles the full flow: checks
auth → shows login/register if needed → accepts invitation → redirects
to workspace. Settings page shows "Copy invite link" button that copies
URL to clipboard. CLI outputs shareable link instead of raw code.
* fix: auto-add workspace creator as owner, integrate auth into pad init
handleCreateWorkspace now adds the authenticated user as owner of the
new workspace immediately — no more relying on the startup backfill.
pad init now checks auth status before making API calls. If no users
exist, prompts to register. If not logged in, prompts to login. After
auth, proceeds with workspace creation normally.
* fix: add join_url to invite response type in API client
* fix: address codex review — invite registration, logout token revocation, workspace scoping
- Allow registration with valid invitation_code (fixes invite flow for new users)
- Revoke Bearer session tokens on logout, not just cookies
- Filter workspace listing to user's memberships (admins see all)
Collections can now define a content_template in their settings — a
markdown template that pre-fills new items. When creating a bug report,
for example, the template can include "Steps to Reproduce", "Expected
Behavior", "Actual Behavior" sections automatically.
- Add content_template to CollectionSettings (Go model + TypeScript type)
- Sidebar "New" button uses template when creating items
- Dashboard quick-create buttons use template
- Template is stored in collection settings JSON, configurable via
the collection edit UI
Full-stack feature: move items between collections (e.g., idea → task)
with automatic field migration.
Backend:
- Field migration engine (items/migrate.go) maps matching fields,
handles type conversions, drops incompatible fields, applies defaults
- Store method updates collection_id and assigns new item_number
- POST /api/v1/workspaces/{ws}/items/{slug}/move endpoint
- Activity logging with "moved" action and from/to metadata
- 6 migration unit tests covering type matching, conversion, and edge cases
CLI:
- pad move <slug> <target-collection> [--field key=value ...]
- Accepts singular collection names (task, idea, bug, etc.)
Web UI:
- "Move to..." dropdown on item detail page
- Shows all collections except current with icons
- Redirects to the item's new URL after move
Field migration rules:
- Same type: transfer directly (validate select options)
- Compatible types (text↔url, number→text, select→text): auto-convert
- Incompatible types: drop silently
- Missing required target fields: apply defaults or error
Export/Import:
- `pad export -o file.json` exports workspace (collections, items,
comments, links, versions) to portable JSON
- `pad import file.json --name X` creates new workspace with
regenerated UUIDs and remapped relations
- GET /workspaces/{slug}/export and POST /workspaces/import endpoints
- "Download JSON" button in workspace settings
- Import tab with drag-and-drop in the create workspace modal
- Full transaction wrapping for atomic imports
Create Workspace Modal:
- Extracted from sidebar dropdown into a proper centered modal
(sidebar's CSS transform was trapping fixed-position elements)
- Rendered at root layout level via uiStore flag
- Create and Import tabs with template picker and file drop zone
Pad — project management for developers and AI agents.
Single Go binary with embedded SvelteKit web UI, SQLite storage,
CLI, and Claude Code /pad skill integration.
https://getpad.dev