buffkit

package module
v0.0.0-...-ba52af2 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2025 License: MIT Imports: 26 Imported by: 0

README ¶

Buffkit

Build Status codecov Go Report Card GoDoc

Go Version License Release Maintenance

PRs Welcome Contributors Issues Forks

🤖 Agentic Development Trial

This project represents an experimental approach to software development using AI agents. We're using BDD-based specifications to deliver a moderately complex problem in a short time frame. This is a proving ground for an agentic development process that leverages:

  • Zed.dev and Warp.dev as our primary development interfaces
  • Anthropic Claude Sonnet and Opus doing the actual development work
  • Graphiti MCP server for persistent agent memory and learning
  • A self-evolving set of rules that guide the AI and enable continuous improvement

We hope this will eventually become a useful tool, but for now it serves as a testbed for exploring how AI agents can effectively collaborate in complex software development tasks.

Our Agentic Development Process

We've developed a structured 4-step approach for building features with AI agents:

  1. High-Level Design & Problem Analysis - Work with the LLM to think through the feature conceptually, discussing the big picture while diving into key details carefully. This mirrors a stand-up meeting or ideation workshop, identifying the feature's shape and potential challenges.

  2. BDD Scenario Creation - Have the LLM stub out features and scenarios in Gherkin format, translating the conceptual design into concrete, testable behaviors.

  3. Human Review & Agreement - Manually review the generated scenarios with your LLM pair to ensure the process makes sense and identify anything missing. This "we agree on the details" step is crucial before implementation begins.

  4. Iterative Implementation - Get busy implementing one feature at a time, using the agreed-upon BDD scenarios as the guiding specification.

This process leverages the strengths of both human intuition and AI precision, ensuring features are well-thought-out before implementation while maintaining the rapid development cycles that agents enable.

The "Hands-Off" Experiment

We're deliberately adopting a hands-off approach to implementation—investing all our effort in teaching and guiding the LLM, then leaving it entirely responsible for writing the code. We'll assess the system holistically afterward. This approach will inevitably fail in multiple ways, but these failures are essential learning opportunities.

To build truly autonomous AI engineers capable of delivering from architectural vision to working software, we must master the art of specification translation—converting loose requirements into detailed implementation plans across every level of abstraction. This demands persistent, methodical guidance of the LLM toward our desired outcomes and approaches.

The assumption that basic rules and memory suffice for complex reasoning is fundamentally flawed. Authentic thought and reasoning require continuous self-reflection, pattern recognition through analogical thinking, and adaptive learning from context. This experiment explores the outer limits of human-AI collaboration where the AI serves as primary implementer while humans concentrate exclusively on specification, guidance, and architectural stewardship.


⚠️ ALPHA STATUS - v0.1.0-alpha

This project is in alpha. Core features are functional but the API may still change. Missing pieces:

  • Grift/CLI tasks for migrations and jobs
  • Some migration SQL files
  • Production deployment guides

Most features documented below are implemented and working.

An opinionated SSR-first stack for Buffalo (Go) that brings Rails-like batteries to server-rendered applications — without the bloat.

What is Buffkit?

Buffkit is a composable plugin system for Buffalo applications that provides:

  • Server-side rendering first with htmx for interactions and SSE for real-time updates
  • Zero JavaScript bundler approach using import maps
  • Batteries included with auth, background jobs, mail, components, and security defaults
  • Database agnostic design using database/sql directly
  • Shadowable templates allowing your app to override any built-in view

Think of it as Rails' conventions and developer experience, but built for Go's performance and simplicity.

Features

Core Packages (âś… = Complete, đźš§ = In Progress)
  • âś… SSR - Server-sent events broker with reconnection, RenderPartial helper for fragments
  • âś… Auth - Full authentication system with registration, login, sessions, password reset
  • âś… Mail - SMTP and dev sender, preview at /__mail/preview in dev mode
  • âś… Jobs - Asynq integration with email and session cleanup handlers
  • âś… Import Maps - Pin/unpin, vendor support, content hashing
  • âś… Security - Headers via unrolled/secure, CSRF middleware
  • âś… Components - Server-side <bk-*> components with slots
  • âś… Migrations - Multi-dialect support (PostgreSQL, MySQL, SQLite)
  • đźš§ CLI Tasks - Grift tasks for migrations and workers (not yet implemented)
Philosophy
  • SSR-first: HTML rendered on the server, htmx for interactions, SSE for push
  • Composable: Install via buffkit.Wire(app, cfg) - each feature is mountable/replaceable
  • Shadowable: Override any template or asset by placing it in your app
  • Zero bundler: JavaScript via import maps, Tailwind via CLI
  • DB-agnostic: Use database/sql - no ORM lock-in

Installation

go get github.com/johnjansen/buffkit

Quick Start

1. Wire Buffkit into your Buffalo app
// actions/app.go
func App() *buffalo.App {
  app := buffalo.New(buffalo.Options{
    // your existing options
  })
  
  // Wire in Buffkit
  kit, err := buffkit.Wire(app, buffkit.Config{
    DevMode:    ENV == "development",
    AuthSecret: []byte(envy.Get("SESSION_SECRET", "change-me-in-production")),
    RedisURL:   envy.Get("REDIS_URL", "redis://127.0.0.1:6379/0"),
    SMTPAddr:   envy.Get("SMTP_ADDR", "localhost:1025"),
    Dialect:    "postgres", // or "sqlite" | "mysql"
  })
  if err != nil {
    return nil
  }
  
  // Use kit.Broker for SSE broadcasts if needed
  app.Use(func(next buffalo.Handler) buffalo.Handler {
    return func(c buffalo.Context) error {
      c.Set("broker", kit.Broker)
      return next(c)
    }
  })
  
  // Your routes
  app.GET("/", HomeHandler)
  
  // Protected routes
  auth := app.Group("/admin")
  auth.Use(buffkit.RequireLogin)
  auth.GET("/dashboard", AdminDashboard)
  
  return app
}
2. Set up JavaScript dependencies
# Install htmx and Alpine.js via import maps
buffalo task importmap:pin htmx.org https://unpkg.com/htmx.org@1.9.12/dist/htmx.js
buffalo task importmap:pin alpinejs https://esm.sh/alpinejs@3.14.1

# Optional: download for offline/vendor
buffalo task importmap:pin htmx.org https://unpkg.com/htmx.org@1.9.12/dist/htmx.js --download
3. Run migrations
# Apply Buffkit migrations (users table, etc.)
buffalo task buffkit:migrate

# Check migration status
buffalo task buffkit:migrate:status
4. Start your app
# Development
buffalo dev

# Start background job worker in another terminal
buffalo task jobs:worker

Usage Examples

Server-Sent Events (SSE)

Broadcast updates to all connected clients:

func UpdateHandler(c buffalo.Context) error {
  broker := c.Value("broker").(*ssr.Broker)
  
  // Render a partial
  html, err := buffkit.RenderPartial(c, "partials/item", map[string]interface{}{
    "item": updatedItem,
  })
  
  // Broadcast to all clients
  broker.Broadcast("item-update", html)
  
  // Also return for htmx request
  return c.Render(200, r.HTML(html))
}
Authentication

Protect routes with the auth middleware:

// Require login for route group
admin := app.Group("/admin")
admin.Use(buffkit.RequireLogin)
admin.GET("/", AdminIndex)

// Or individual routes
app.GET("/profile", buffkit.RequireLogin(ProfileHandler))

Customize the user store:

type MyUserStore struct {
  db *sql.DB
}

func (s *MyUserStore) ByEmail(ctx context.Context, email string) (*buffkit.User, error) {
  // Your implementation
}

// In app setup
buffkit.UseUserStore(&MyUserStore{db: db})
Background Jobs

Define and enqueue jobs:

// Define a job handler
kit.Jobs.Mux.HandleFunc("email:welcome", func(ctx context.Context, t *asynq.Task) error {
  var payload struct {
    UserID string `json:"user_id"`
  }
  if err := json.Unmarshal(t.Payload(), &payload); err != nil {
    return err
  }
  
  // Send welcome email
  return kit.Mail.Send(ctx, buffkit.Message{
    To:      user.Email,
    Subject: "Welcome!",
    HTML:    welcomeHTML,
  })
})

// Enqueue a job
task := asynq.NewTask("email:welcome", payload)
kit.Jobs.Client.Enqueue(task)
Server Components

Use server-side components in templates:

<!-- templates/index.plush.html -->
<bk-button href="/save" variant="primary">
  Save Changes
</bk-button>

<bk-card>
  <bk-slot name="header">
    <h2>Welcome</h2>
  </bk-slot>
  <p>This is the card body content.</p>
</bk-card>

<bk-dropdown>
  <bk-slot name="trigger">Options</bk-slot>
  <a href="/profile">Profile</a>
  <a href="/settings">Settings</a>
  <a href="/logout" data-method="POST">Logout</a>
</bk-dropdown>
Mail Sending
// Send email
err := kit.Mail.Send(ctx, buffkit.Message{
  To:      "user@example.com",
  Subject: "Your order is confirmed",
  HTML:    orderConfirmationHTML,
  Text:    orderConfirmationText,
})

// In development, preview emails at /__mail/preview

Configuration

type Config struct {
  DevMode    bool      // Enable dev tools like mail preview
  AuthSecret []byte    // Session encryption key
  RedisURL   string    // For background jobs
  SMTPAddr   string    // SMTP server address
  SMTPUser   string    // SMTP username
  SMTPPass   string    // SMTP password
  Dialect    string    // "postgres" | "sqlite" | "mysql"
}

Environment variables:

  • DATABASE_URL - Used by migration tasks
  • REDIS_URL - Redis connection for jobs
  • SESSION_SECRET - Secret key for session cookies
  • SMTP_ADDR - SMTP server (e.g., "smtp.sendgrid.net:587")

Template & Asset Overrides

Buffkit templates and assets can be overridden by creating files at the same paths in your app:

your-app/
├── templates/
│   ├── auth/
│   │   └── login.plush.html    # Override login page
│   └── components/
│       └── button.plush.html   # Override bk-button component
└── public/
    └── assets/
        └── js/
            └── sse-client.js   # Override SSE client

Database Migrations

Buffkit uses simple SQL migrations without ORM dependencies:

buffkit/db/migrations/
├── auth/
│   ├── 0001_users.up.sql
│   └── 0001_users.down.sql
└── mail/
    └── 0001_outbox.up.sql

Commands:

  • buffalo task buffkit:migrate - Apply all pending migrations
  • buffalo task buffkit:migrate:status - Show migration status
  • buffalo task buffkit:migrate:down N - Rollback last N migrations

Architecture

flowchart LR
  subgraph App["Your Buffalo App"]
    A1["actions/*"]
    T1["templates/*"]
    P1["public/assets/*"]
    DB[(database/sql)]
  end

  subgraph Buffkit
    Wire["buffkit.Wire()"]
    SSR["SSE + Fragments"]
    AUTH["Session Auth"]
    JOBS["Background Jobs"]
    MAIL["Email Sending"]
    IMAP["Import Maps"]
    SEC["Security Headers"]
    COMP["Server Components"]
  end

  Wire --> SSR & AUTH & JOBS & MAIL & IMAP & SEC & COMP
  App --> Buffkit
  DB --- AUTH & JOBS & MAIL

Testing

// Test with fake stores
func TestLogin(t *testing.T) {
  app := buffalo.New(buffalo.Options{})
  
  kit, _ := buffkit.Wire(app, buffkit.Config{
    DevMode: true,
    AuthSecret: []byte("test-secret"),
  })
  
  // Use in-memory user store for testing
  buffkit.UseUserStore(&FakeUserStore{})
  
  req := httptest.NewRequest("POST", "/login", strings.NewReader("email=test@example.com&password=secret"))
  res := httptest.NewRecorder()
  
  app.ServeHTTP(res, req)
  
  require.Equal(t, 302, res.Code)
  require.Contains(t, res.Header().Get("Set-Cookie"), "session=")
}

Tasks

Buffkit provides several grift tasks:

  • buffkit:migrate - Run database migrations
  • buffkit:migrate:status - Show migration status
  • buffkit:migrate:down N - Rollback N migrations
  • importmap:pin NAME URL [--download] - Add JavaScript dependency
  • importmap:print - Output import map HTML
  • jobs:worker - Start background job worker

Requirements

  • Go 1.21+
  • Buffalo v0.18+
  • Redis (for background jobs)
  • PostgreSQL, MySQL, or SQLite

Contributing

This project follows the philosophy of simplicity and clarity:

  • Keep it simple, keep it minimal
  • One step, one purpose, one concern at a time
  • Clarity beats cleverness
  • Test everything

License

MIT

Status

v0.1 - Draft Implementation

This is an early implementation of the Buffkit specification. The API may change as we refine the developer experience. We're building this to replace Rails/Loco for 2025-era server-rendered applications.

Credits

Built with inspiration from Rails' conventions, but designed for Go's simplicity and performance.

Documentation ¶

Overview ¶

Package buffkit provides an opinionated SSR-first stack for Buffalo applications. It brings Rails-like batteries to server-rendered Go applications without the bloat.

Buffkit is designed around several core principles:

  • Server-side rendering first (HTML rendered on server, htmx for interactions)
  • Zero JavaScript bundler (uses import maps instead)
  • Batteries included (auth, jobs, mail, components out of the box)
  • Database agnostic (uses database/sql, no ORM lock-in)
  • Everything is shadowable (your app can override any template or asset)

The main entry point is the Wire() function which installs all Buffkit packages into your Buffalo application with a single call.

Index ¶

Constants ¶

This section is empty.

Variables ¶

This section is empty.

Functions ¶

func NewMigrationRunner ¶

func NewMigrationRunner(db *sql.DB, migrationFS embed.FS, dialect string) *migrations.Runner

NewMigrationRunner creates a new migration runner. It uses the new migrations package implementation.

func RenderPartial ¶

func RenderPartial(c buffalo.Context, name string, data map[string]interface{}) ([]byte, error)

RenderPartial renders a partial template with data. This is a helper for rendering fragments that can be used for both htmx responses AND SSE broadcasts - ensuring single source of truth for HTML fragments:

html, err := buffkit.RenderPartial(c, "partials/item", map[string]interface{}{
    "item": item,
})
// Send as htmx response
c.Render(200, r.HTML(html))
// AND broadcast via SSE
kit.Broker.Broadcast("item-update", html)

func RequireLogin ¶

func RequireLogin(next buffalo.Handler) buffalo.Handler

RequireLogin is middleware that ensures user is authenticated. Add this to routes or groups that need protection:

app.GET("/admin", buffkit.RequireLogin(AdminHandler))
// or
protected := app.Group("/admin")
protected.Use(buffkit.RequireLogin)

If the user is not logged in, they are redirected to /login. If authenticated, the user object is added to context as "current_user".

func SetGlobalKit ¶

func SetGlobalKit(kit *Kit)

SetGlobalKit sets the global Kit instance for Grift tasks This is called automatically by Wire()

func Version ¶

func Version() string

Version returns the current Buffkit version. This is useful for debugging and ensuring compatibility.

Types ¶

type Config ¶

type Config struct {
	// DevMode enables development features like mail preview at /__mail/preview
	// and relaxes certain security restrictions. Should be false in production.
	DevMode bool

	// AuthSecret is used for session encryption. This MUST be set to a secure
	// random value in production. The session cookies are encrypted with this key.
	// Required field - Wire() will error if not provided.
	AuthSecret []byte

	// RedisURL for background job processing via Asynq. If empty, job enqueuing
	// becomes a no-op (useful for development without Redis). Format:
	// "redis://username:password@localhost:6379/0"
	RedisURL string

	// SMTP configuration for mail sending. If SMTPAddr is empty, a development
	// mail sender is used that logs emails instead of sending them.
	SMTPAddr string // Host:port (e.g., "smtp.sendgrid.net:587")
	SMTPUser string // SMTP username for authentication
	SMTPPass string // SMTP password for authentication

	// Database dialect: "postgres" | "sqlite" | "mysql"
	// This is used for dialect-specific SQL in migrations and stores.
	Dialect string

	// Optional database connection. If not provided, Buffkit will attempt to
	// connect using the DATABASE_URL environment variable. This allows you to
	// either manage the connection yourself or let Buffkit handle it.
	DB *sql.DB
}

Config holds all configuration for Buffkit packages. This is the main configuration struct that controls how Buffkit behaves. Each field maps to a specific subsystem's configuration needs.

type Kit ¶

type Kit struct {
	// SSR broker for server-sent events. Use this to broadcast real-time
	// updates to connected clients: kit.Broker.Broadcast("event", htmlBytes)
	Broker *ssr.Broker

	// Jobs runtime for background processing. Access the Asynq client to
	// enqueue jobs: kit.Jobs.Client.Enqueue(task)
	Jobs *jobs.Runtime

	// Mail sender interface. Can be used directly to send emails:
	// kit.Mail.Send(ctx, message)
	Mail mail.Sender

	// Auth store for user management. Useful if you need to directly
	// query users: kit.AuthStore.ByEmail(ctx, email)
	AuthStore auth.UserStore

	// Import map manager for JavaScript dependencies. Can be used to
	// dynamically add pins: kit.ImportMap.Pin("name", "url")
	ImportMap *importmap.Manager

	// Component registry for server-side components. Register custom
	// components: kit.Components.Register("my-component", renderer)
	Components *components.Registry

	// Configuration that was used to initialize Buffkit. Useful for
	// checking settings at runtime.
	Config Config
}

Kit holds references to all Buffkit subsystems after wiring. This is returned from Wire() and provides access to all the initialized components. You can use these references to interact with Buffkit systems directly when needed (e.g., broadcasting SSE events, enqueuing jobs).

func Wire ¶

func Wire(app *buffalo.App, cfg Config) (*Kit, error)

Wire installs all Buffkit packages into a Buffalo application. This is the main integration point - call this once in your app.go:

app := buffalo.New(buffalo.Options{...})
kit, err := buffkit.Wire(app, buffkit.Config{
    DevMode:    ENV == "development",
    AuthSecret: []byte(envy.Get("SESSION_SECRET", "change-me")),
    RedisURL:   envy.Get("REDIS_URL", ""),
})

Wire performs the following setup:

  1. Validates configuration (ensures required fields are set)
  2. Initializes SSR broker and mounts /events endpoint
  3. Sets up authentication with login/logout routes
  4. Configures background job processing (if Redis available)
  5. Initializes mail sending (SMTP or dev mode)
  6. Sets up import maps with default JavaScript libraries
  7. Adds security middleware to the request chain
  8. Registers default server-side components
  9. Adds helper functions to Buffalo context

The order of initialization matters as some systems depend on others. Wire handles this ordering correctly.

func (*Kit) Shutdown ¶

func (k *Kit) Shutdown()

Shutdown gracefully shuts down the Kit and all its subsystems. This should be called when the application is shutting down to prevent goroutine leaks and ensure proper cleanup of resources.

type MigrationRunner ¶

type MigrationRunner struct {
	// Database connection to run migrations against
	DB *sql.DB

	// Dialect for database-specific SQL ("postgres", "sqlite", "mysql")
	Dialect string

	// Table name for tracking migrations (defaults to "buffkit_migrations")
	Table string
}

MigrationRunner handles database migrations for Buffkit. It manages the buffkit_migrations table that tracks which migrations have been applied. Migrations are simple SQL files that are run in lexical order.

Directories ¶

Path Synopsis
cmd
grift command
Package components provides server-side custom elements for Buffalo applications.
Package components provides server-side custom elements for Buffalo applications.
Package ssr provides server-sent events (SSE) functionality for real-time updates.
Package ssr provides server-sent events (SSE) functionality for real-time updates.

Jump to

Keyboard shortcuts

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