pickle

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 5, 2026 License: MIT

README

Pickle

Building secure foundations for agentic software development.

Pickle is a Go code generation framework for backend applications that need to be understandable by people, auditable by tools, and safe for AI agents to modify. You write controllers, migrations, request classes, and middleware. Pickle generates plain, idiomatic Go. The output compiles to a single static binary with no runtime dependency on Pickle.

You write controllers.     Pickle generates models.
You write migrations.      Pickle generates query builders.
You write migrations.      Pickle generates a GraphQL API.
You write request classes.  Pickle generates validation + deserialization.
You write routes.go.       Pickle wires it all together.

The generated code is readable, debuggable, and grep-friendly. The goal is not magic; it is explicit code generated from explicit constraints.

No Lock-In

Pickle gives you leverage while you are building, but it does not own your application.

pickle export converts a Pickle project into a standalone Go app with idiomatic GORM models, SQL migrations, config, routes, and controllers. The output has no Pickle imports and no generated framework dependency.

Use Pickle for velocity. Export when you want plain Go.

Frameworks usually ask for trust. Pickle earns it by letting you leave.

Why Pickle?

AI agents are most useful when the codebase gives them stable structure, typed constraints, and fast feedback. Pickle is designed around that idea: the source of truth is small, conventional, and queryable; generated code is ordinary Go; and framework-aware static analysis catches common backend mistakes before deployment.

Pickle reduces the number of security-critical decisions developers and agents have to make by hand. Some unsafe patterns are avoided by generated APIs, some are made visible by convention, and others are flagged by Squeeze before code ships.

Constrained by generated APIs:

  • SQL injection — The generated query builder uses parameterized queries exclusively. Application code uses typed query methods instead of interpolating SQL strings.
  • Mass assignment — Request structs define exactly which fields are accepted. If CreateUserRequest doesn't have a Role field, POSTing {"role": "admin"} does nothing. The model never sees unvalidated input.
  • Validation bypass — Controllers use generated binding functions that deserialize and validate before returning typed request structs.
  • Encryption at rest — Columns marked .Encrypted() are transparently encrypted before storage and decrypted on read. Columns marked .Sealed() are write-only — they can be verified but never retrieved in plaintext. The query builder enforces both: no range queries on encrypted columns, no WHERE clauses on sealed columns.
  • Data tampering — Immutable and append-only tables are cryptographically hash-chained. Every row's row_hash includes the previous row's hash — tampering with any historical record breaks the chain. Periodic Merkle tree checkpoints give O(log n) inclusion proofs you can hand to an auditor.

Constrained RBAC and actions:

  • Ungated actions — every action requires a gate function. The generator refuses to produce output if a gate is missing. The action method is renamed to unexported in the compiled output, so it can only be called through the gated model method.
  • Role visibility leaks — column annotations (ComplianceSees(), SupportSees()) generate SelectFor(role) query scopes. Unknown roles see only Public() columns. Manages() roles see everything. Squeeze flags controllers that query role-annotated models without calling SelectFor*.
  • Audit trail gaps — every successful action execution writes an append-only audit row in the same database transaction as the action. Both succeed or both roll back. No action persists without its audit record.

Visible by convention:

  • Every endpoint, its middleware stack, and its grouping are in one file: routes/web.go. A missing Auth, LoadRoles, or RequireRole is visible to reviewers, static tools, and AI agents working in the project.

Caught at build time by Squeeze:

  • IDOR (Insecure Direct Object Reference) — Squeeze traces route -> middleware -> controller -> query and checks whether protected resource access is scoped by owner in conventional Pickle code. This works because migrations define ownership columns, the router defines middleware, and controllers use generated query scopes.
  • Data leakage, unbounded queries, missing rate limits, enum validation, UUID panics, missing required fields — flagged before deployment. See Squeeze below.

Standard security tooling works out of the box. Generated code is plain Go — go vet, gosec, staticcheck, Snyk, and Semgrep work with zero configuration. No framework abstractions to unwrap. Security scanners see exactly what runs in production.

Squeeze: Static Analysis for Pickle

pickle squeeze is static security analysis that understands Pickle projects: routes, middleware, migrations, request classes, generated query builders, RBAC policies, and actions. It complements generic Go linters by checking framework-level invariants.

pickle squeeze              # Run full validation
pickle squeeze --hard       # Strict mode: warnings become failures
Analyzing Pickle project...
No findings.

If something's wrong, Squeeze tells you exactly where:

Analyzing Pickle project...

  app/http/controllers/post_controller.go
    line 28 [ownership_scoping] PUT /api/posts/:id — query not scoped by owner (IDOR)

Found 1 error(s), 0 warning(s)
Rules
Rule Severity What it catches
ownership_scoping error Write routes (PUT/PATCH/DELETE) behind auth that don't scope queries by owner — IDOR vulnerabilities
read_scoping error Read routes (GET) behind auth that don't scope queries by owner — data leakage
public_projection error Unauthenticated routes returning model data without .Public() — leaks sensitive fields
unbounded_query error .All() without .Limit() — denial-of-service vector
rate_limit_auth error Auth endpoints (login, register) without rate limiting middleware
enum_validation error Status/role/type fields without oneof= validation — accepts arbitrary values
uuid_error_handling error uuid.MustParse() on user input — panics crash the server
required_fields error Create() calls missing NOT NULL fields — database rejects the insert
no_printf warning fmt.Print* in controllers — use structured logging
param_mismatch error Route parameters (:id) with no corresponding ctx.Param() call, or vice versa
auth_without_middleware error ctx.Auth() called in a controller without auth middleware on the route
immutable_raw_update error Raw UPDATE on an immutable or append-only table — use the query builder
immutable_raw_delete error Raw DELETE on an immutable table without SoftDeletes()
immutable_timestamps error t.Immutable() + t.Timestamps() on the same table — timestamps are derived from UUID v7
integrity_hash_override error Raw SQL setting row_hash or prev_hash — these are computed by the query builder
encrypted_column_range error Range/comparison scopes (GT, LT, Between) on .Encrypted() columns — ciphertext ordering is meaningless
sealed_column_where error Any WHERE clause on a .Sealed() column — sealed data cannot be queried
encrypted_column_order_by error ORDER BY on an .Encrypted() column — ciphertext sort order is random
encrypted_sealed_conflict error Column marked both .Encrypted() and .Sealed() — pick one
encrypted_missing_key_config error .Encrypted() columns exist but no encryption key is configured
stale_role_annotation warning Migration uses XxxSees() for a role removed via policy
unknown_role_annotation error Migration uses XxxSees() for a role that has never been defined
role_without_load error RequireRole() used but LoadRoles not in middleware chain
default_role_missing error Policies exist but no role has .Default(), or multiple do
ungated_action error Action exists with no corresponding gate
direct_execute_call error Action method called directly instead of through the gated model method
scope_builder_leak error ScopeBuilder referenced outside database/scopes/
query_builder_in_scope error XxxQuery referenced inside database/scopes/

Run Squeeze in CI so generated constraints and handwritten code are checked together.

# .github/workflows/squeeze.yml
- name: Run Pickle static analysis
  run: pickle squeeze --hard
Built for Agentic Development

Pickle is designed for collaboration between developers, AI agents, and static tools. Every convention serves two audiences: the developer who needs to ship and the model that needs to make correct changes with limited context.

A functioning Pickle app is ~2,000 tokens of source. Controllers are pure business logic — no boilerplate to read past. Request structs are self-documenting API contracts. Migrations are the single source of truth for schema. An agent does not need to parse framework wiring to understand what an endpoint does.

Pickle ships an MCP server that gives AI agents queryable access to your project's structure without dumping source files into context.

pickle schema:show transfers    → exact table structure with visibility annotations
pickle routes:list              → every endpoint, middleware, request class
pickle roles:list               → all RBAC roles with permissions
pickle roles:show admin         → single role with column visibility and action grants
pickle graphql:list             → exposed GraphQL models with operations
pickle make:controller          → scaffold via tooling, not by writing boilerplate

The model can query constraints instead of inferring them from scattered source files. It can discover what fields exist, what is validated, what middleware protects each route, and what relationships are defined through structured tool calls.

The practical goal is simple: reduce the context required for humans and agents to make correct, security-aware changes.


Getting Started

See the Getting Started guide to create your first Pickle project.

Documentation

Topic Description
Getting Started Create your first Pickle project
Controllers Handling requests and returning responses
Middleware Auth, rate limiting, and request pipeline
Requests Validation and deserialization
Migrations Database schema as code
Views Database views with computed columns
Router Route definitions and groups
Context The request context object
Response Building HTTP responses
QueryBuilder Typed database queries
Config Application configuration
Commands CLI commands reference
Tickle The preprocessor pipeline
Squeeze Static security analysis
GraphQL Auto-generated GraphQL API from migrations
Cron Jobs Scheduled background tasks
Encryption Encryption at rest and sealed columns
RBAC Role-based access control, column visibility, role-aware queries
Policies Role policies and GraphQL exposure policies
Actions Gated actions, scopes, and audit trails
Ledger Example Immutable tables, append-only tables, DB permissions
Immutable Tables & Cryptographic Integrity

Financial records, audit logs, compliance data — anything where history matters. Declare t.Immutable() or t.AppendOnly() in your migration and Pickle enforces it at every layer.

m.CreateTable("transactions", func(t *Table) {
    t.AppendOnly()
    t.UUID("account_id").NotNull().ForeignKey("accounts", "id")
    t.String("type", 20).NotNull()
    t.Decimal("amount", 18, 2).NotNull()
    t.String("currency", 3).NotNull()
})

What you get:

Mutable Immutable Append-Only
DSL t.Timestamps() t.Immutable() t.AppendOnly()
Create() INSERT INSERT INSERT
Update() UPDATE INSERT new version Not generated
Delete() DELETE INSERT with deleted_at* Not generated
Hash chain No Yes Yes
Merkle proofs No Yes Yes
DB permissions needed SELECT, INSERT, UPDATE, DELETE SELECT, INSERT SELECT, INSERT

* Only with t.SoftDeletes(). Without it, Delete() is not generated — immutable tables without soft deletes have no deletion concept.

Developer code is identical to mutable tables:

// Create — hash chain extended automatically
transfer := &models.Transfer{CustomerID: id, Amount: amount, Status: "pending"}
models.QueryTransfer().Create(transfer)

// Read — always returns the latest version, transparently
transfer, _ := models.QueryTransfer().WhereID(id).First()

// Update — inserts a new version, old version preserved forever
transfer.Status = "completed"
models.QueryTransfer().Update(transfer)

// Full history — opt-in only
versions, _ := models.QueryTransfer().WhereID(id).AllVersions().All()

Cryptographic verification:

// Verify the full hash chain — O(n), run as a periodic audit
err := models.QueryTransaction().VerifyChain()

// Create a Merkle checkpoint — O(n) within the checkpoint window
cp, _ := models.QueryTransaction().Checkpoint()

// Generate an inclusion proof for an auditor — O(log n)
proof, _ := models.QueryTransaction().Proof(transaction)
ok := models.VerifyProof(proof) // pure function, no DB needed

Every row is chained to its predecessor via SHA-256. Merkle tree checkpoints roll the chain into a binary tree for efficient verification. Tampering with any historical row breaks the chain — detectable by VerifyChain() and provable via VerifyProof().

Three layers reinforce the invariant: schema DSL (unsafe methods are not generated), Go compiler (missing methods cannot be called), and database permissions (SELECT + INSERT only for immutable tables). Together, they make the intended data model easier to verify and audit.

Cron Jobs

Schedule recurring tasks with pickle make:job. Jobs run inside your compiled binary — no external cron daemon needed. Define the schedule, write the logic, and Pickle wires it into the app lifecycle. See the Cron Jobs docs for details.

The Stack

Migrations → Models → Query Builders → Controllers → Routes
Policies   → Roles  → Gates          → Actions     → Audit Trail
     ↑ single source of truth              ↑ pure intent

Everything flows from migrations. Everything is queryable via MCP. Everything is verifiable via Squeeze. The generated output is plain Go with zero dependency on Pickle.

Contributing

Pickle is open to contributions. Here's how to get started:

git clone https://github.com/shortontech/pickle.git
cd pickle
go run ./pkg/tickle/cmd/                                        # regenerate embedded templates
go build ./...                                                   # build
go run ./cmd/pickle/ generate --project ./testdata/basic-crud/   # generate the test app
go run ./cmd/pickle/ squeeze --project ./testdata/basic-crud/    # run static analysis
go test ./...                                                    # test

Tickle-generated embeds and testdata output are gitignored. You generate them locally.

Before submitting a PR:

  1. Run go run ./pkg/tickle/cmd/ — always, not just if you think you changed something
  2. Run go run ./cmd/pickle/ generate --project ./testdata/basic-crud/
  3. Run go run ./cmd/pickle/ squeeze --project ./testdata/basic-crud/ — must pass clean
  4. Run go test ./... — all tests must pass

Guidelines:

  • Generated files (*_gen.go) are never edited by hand. Change the source in pkg/cooked/, pkg/schema/, or the generator, then regenerate.
  • Squeeze rules should be precise. If a rule fires, it should point to a real risk. Noisy rules get disabled by users and stop providing value.
  • Security is the priority. If a change weakens a security invariant for convenience, it needs a strong justification.
  • Keep the dependency list minimal. Pickle's output has zero dependency on Pickle. New runtime dependencies need strong justification.

Expressive DX. Go binary. No runtime. Agent-ready by design.

Directories

Path Synopsis
cmd
pickle command
pkg
mcp
tickle/cmd command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL