Like Django, but made by Juan in Go.
A scaffolding CLI and reusable libraries for building Go + Vite/React web applications with batteries included: OIDC authentication, session management, admin mode, audit logging, and more.
This project has been created with Claude (Anthropic's AI assistant). I had several internal projects that shared the same patterns - OIDC auth, session management, admin mode, etc. - but the code was just copy-pasted between them with no actual shared library. I used Claude to extract the minimum common denominator from these projects and refactor it into this reusable framework. The patterns come from real-world production code, but the extraction, refactoring, and documentation were done through AI pair programming.
- CLI scaffolding -
juango init myappcreates a complete project structure - OIDC authentication - Plug-and-play with any OIDC provider (Entra ID, Okta, Auth0, etc.)
- First-user bootstrap - First OIDC login is automatically promoted to admin
- Session management - Secure cookie-based sessions with SQLite backend
- Admin mode - Time-limited elevated privileges with audit logging
- User impersonation - Debug issues as another user (admin only)
- Admin API - User management, stats, and audit log querying with filtering/pagination
- Audit logging - Track all sensitive operations with actor/impersonator awareness
- Health endpoint -
GET /api/healthwith database connectivity check - Frontend serving - Dev proxy to Vite, embedded SPA in production
- SQLite with WAL - Simple, fast, single-file database
- Background tasks - Asynq integration for Redis-backed job queues
- React UI library - Pre-built admin pages, components with shadcn/ui styling
- Generated CLI commands -
make-admin,list-admins,remove-admin,db backup,db stats
go install github.com/juanfont/juango@latest# Create a new project
juango init myapp
cd myapp
# Configure OIDC (edit config.yml with your provider details)
cp config.example.yml config.yml
vim config.yml
# Start development (runs Vite + Go concurrently)
juango dev
# Visit http://localhost:8080Using an AI coding agent? See
AGENTS.mdfor a concise guide to bootstrapping and extending a project with juango. Every generated project also ships its ownAGENTS.mdtailored to that app.
A generated project looks like this:
myapp/
├── cmd/myapp/
│ ├── myapp.go # Entry point
│ └── cli/
│ ├── root.go # CLI setup
│ ├── serve.go # Server command
│ ├── make_admin.go # Promote user to admin
│ ├── list_admins.go # List admin users
│ ├── remove_admin.go # Revoke admin from user
│ ├── worker.go # Background task worker
│ └── db.go # Database utilities (backup, stats)
├── internal/
│ ├── api/
│ │ ├── app.go # API routes and handlers
│ │ └── tasks.go # Task-enqueueing endpoints
│ ├── database/
│ │ ├── db.go # Database wrapper
│ │ ├── README.md # Schema & squibble migration guide
│ │ └── sql/
│ │ └── schema.sql # Your tables
│ ├── tasks/
│ │ ├── registry.go # Task handler registration
│ │ └── dummy.go # Example task
│ └── types/
│ └── config.go # Configuration struct
├── frontend/
│ ├── src/
│ │ ├── App.tsx # React app
│ │ ├── pages/ # Your pages (including admin/)
│ │ ├── components/ # Your components
│ │ ├── contexts/ # Auth, Admin, Breadcrumb contexts
│ │ └── lib/ # API client, types, utilities
│ ├── src/test/ # Vitest setup
│ ├── package.json
│ └── vite.config.ts
├── app.go # Main application
├── justfile # Dev commands (dev, build, serve, worker, test)
├── AGENTS.md # Guidance for AI coding agents
├── go.mod
├── config.example.yml
└── .gitignore
Creates a new project with all the scaffolding.
juango init myapp # Uses github.com/user/myapp
juango init myapp -m github.com/org/myapp # Custom module pathStarts the development environment:
- Vite dev server with HMR on port 5173
- Go server on port 8080 (proxies to Vite for frontend)
cd myapp
juango devShows version information.
Every scaffolded project includes these commands out of the box:
Starts the HTTP server.
myapp serve -c config.ymlPromotes a user to admin by email address.
myapp make-admin -c config.yml user@example.comLists all admin users.
myapp list-admins -c config.ymlRevokes admin privileges from a user. Refuses to remove the last admin.
myapp remove-admin -c config.yml user@example.comCreates a database backup using SQLite VACUUM INTO.
myapp db backup -c config.yml /backups/myapp.dbShows database statistics: user count, admin count, audit log entries, and file size.
myapp db stats -c config.ymlRuns the background task worker (Asynq). Requires tasks.enabled: true in config
and a reachable Redis. Register handlers in internal/tasks/registry.go.
myapp worker -c config.ymlThe github.com/juanfont/juango module provides reusable packages:
SPA serving with automatic dev/prod detection.
import "github.com/juanfont/juango/frontend"
//go:embed frontend/dist
var frontendFS embed.FS
func main() {
router := mux.NewRouter()
frontend.Setup(router, frontendFS, "frontend/dist")
}OIDC authentication and session middleware.
import "github.com/juanfont/juango/auth"
// Create OIDC provider
provider, _ := auth.NewOIDCProvider(ctx, auth.OIDCProviderConfig{
ServerURL: "http://localhost:8080",
OIDCConfig: types.OIDCConfig{
Issuer: "https://login.microsoftonline.com/...",
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
},
})
// Setup handlers
handlers := auth.NewOIDCHandlers(provider, sessionStore, "session", userStore, auditLogger)
router.HandleFunc("/api/auth/login", handlers.LoginHandler)
router.HandleFunc("/api/auth/logout", handlers.LogoutHandler).Methods("POST")
router.HandleFunc(provider.CallbackPath(), handlers.CallbackHandler)
// Enable first-user bootstrap (first OIDC login gets auto-promoted to admin)
handlers.WithUserCounter(database)
// Protect routes
middleware := auth.NewSessionMiddleware(sessionStore, "session", userStore, auditLogger, 30*time.Minute)
router.HandleFunc("/api/protected", middleware.RequireAuth(myHandler))
router.HandleFunc("/api/admin-only", middleware.RequireAuth(middleware.RequireAdmin(adminHandler)))Interfaces: Your database layer implements these to integrate with the auth system:
auth.UserStore-CreateOrUpdateUserFromClaim,GetUserByIDauth.AuditLogger-CreateAuditLogauth.AdminUserStore- Extends UserStore withGetAllUsers,GetUserByEmail,UpdateUser,CountUsers,CountAdminUsers,CountActiveUsers,CountNewUsersauth.AdminAuditStore- Extends AuditLogger withGetAuditLogs,CountAuditLogs,GetDistinctActions,GetDistinctResourceTypesauth.UserCounter-CountUsers(used for first-user bootstrap)
Admin mode, impersonation, user management API, audit log API, and health check.
import "github.com/juanfont/juango/admin"
// Admin mode and impersonation
handlers := admin.NewHandlers(sessionStore, "session", userStore, auditLogger, 30*time.Minute)
router.HandleFunc("/api/admin/mode/enable", middleware.RequireAdmin(handlers.AdminModeEnableHandler)).Methods("POST")
router.HandleFunc("/api/admin/mode/disable", middleware.RequireAdmin(handlers.AdminModeDisableHandler)).Methods("POST")
router.HandleFunc("/api/admin/mode/status", handlers.AdminModeStatusHandler).Methods("GET")
router.HandleFunc("/api/admin/impersonate/start", middleware.RequireAdminMode(handlers.ImpersonationStartHandler)).Methods("POST")
router.HandleFunc("/api/admin/impersonate/stop", handlers.ImpersonationStopHandler).Methods("POST")
// Admin API (user management + audit logs)
adminAPI := admin.NewAdminAPIHandlers(adminUserStore, adminAuditStore)
router.HandleFunc("/api/admin/users/stats", middleware.RequireAdminMode(adminAPI.GetUserStatsHandler)).Methods("GET")
router.HandleFunc("/api/admin/users", middleware.RequireAdminMode(adminAPI.ListUsersHandler)).Methods("GET")
router.HandleFunc("/api/admin/users/{id}", middleware.RequireAdminMode(adminAPI.GetUserHandler)).Methods("GET")
router.HandleFunc("/api/admin/users/{id}", middleware.RequireAdminMode(adminAPI.UpdateUserHandler)).Methods("PATCH")
router.HandleFunc("/api/admin/audit-logs/stats", middleware.RequireAdminMode(adminAPI.GetAuditLogStatsHandler)).Methods("GET")
router.HandleFunc("/api/admin/audit-logs", middleware.RequireAdminMode(adminAPI.ListAuditLogsHandler)).Methods("GET")
// Health check (no auth required)
healthHandler := admin.NewHealthHandler(database) // database implements admin.DBPinger
router.HandleFunc("/api/health", healthHandler.ServeHTTP).Methods("GET")Common HTTP middleware.
import "github.com/juanfont/juango/middleware"
router.Use(middleware.Logging(logger)) // Request logging with zerolog
router.Use(middleware.Metrics()) // Prometheus metrics
router.Use(middleware.Recovery()) // Panic recovery
router.Use(middleware.CORS(nil)) // CORS (nil = permissive defaults)SQLite helpers with WAL mode and migrations.
import "github.com/juanfont/juango/database"
//go:embed sql/schema.sql
var schema string
db, _ := database.New("app.db", schema)
defer db.Close()
// Transactions
db.WithTx(ctx, func(tx *sqlx.Tx) error {
// ...
return nil
})Asynq task queue wrappers.
import "github.com/juanfont/juango/tasks"
// Client (enqueue tasks): NewClient(redisAddr, redisPassword, redisDB)
client := tasks.NewClient("localhost:6379", "", 0)
defer client.Close()
client.Enqueue("email:send", payload) // returns (*asynq.TaskInfo, error)
// Server (process tasks)
cfg := tasks.DefaultServerConfig("localhost:6379", "", 0)
server := tasks.NewServer(cfg)
server.HandleFunc("email:send", handleSendEmail)
server.Run() // blocks until shutdownCommon types used across packages.
import "github.com/juanfont/juango/types"
// Core types
// types.User, types.OIDCClaims, types.OIDCConfig
// types.AdminModeState, types.ImpersonationState
// Audit logging
// types.AuditLog, types.AuditLogFilter
// Admin API
// types.UserStats, types.AuditLogStats
// types.UpdateUserRequest, types.HealthResponseScaffolded projects include a complete React frontend with:
- Contexts:
AuthContext,AdminContext,BreadcrumbContext - Components:
ProtectedRoute,Layout,AdminGuard,AdminModeToggle,ImpersonationBanner,AuditLogDetailModal - Admin pages:
admin/Users(user management with sorting/pagination),admin/AuditLogs(filterable logs with CSV export) - UI components: Button, Card, Dialog, Table, Badge, Select, Input, Tooltip, etc. (shadcn/ui)
- Utilities:
ApiClientfor backend communication,cn()for class names
import { Routes, Route } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import { AdminProvider } from './contexts/AdminContext'
import { ProtectedRoute } from './components/ProtectedRoute'
import Layout from './components/Layout'
export default function App() {
return (
<AuthProvider>
<AdminProvider>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<ProtectedRoute><Layout /></ProtectedRoute>}>
<Route index element={<Home />} />
</Route>
</Routes>
</AdminProvider>
</AuthProvider>
)
}Example config.yml:
listen_addr: ":8080"
advertise_url: "http://localhost:8080"
admin_mode_timeout: 30m
database:
path: "myapp.db"
session:
cookie_name: "myapp_session"
cookie_expiry: 24h
authentication_key: "32-byte-key-for-authentication!!" # exactly 32 bytes
encryption_key: "32-byte-key-for-encryption-here" # exactly 32 bytes
oidc:
issuer: "https://login.microsoftonline.com/{tenant}/v2.0"
client_id: "your-client-id"
client_secret: "your-client-secret"
scopes:
- openid
- profile
- email
# Background tasks (opt-in). When enabled, the app connects to Redis and the
# `worker` command processes queued jobs.
tasks:
enabled: false
redis:
addr: "localhost:6379"
logging:
level: info
format: textGenerate secure keys:
openssl rand -hex 16 # generates 32-character hex stringEvery scaffolded project comes with these endpoints pre-wired:
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/health |
GET | None | Health check with database status |
/api/auth/login |
GET | None | Initiates OIDC login flow |
/api/auth/logout |
POST | None | Destroys session |
/api/auth/session |
GET | None | Returns current session info |
/api/admin/mode/status |
GET | Auth | Admin mode status |
/api/admin/mode/enable |
POST | Admin | Enable admin mode (with reason) |
/api/admin/mode/disable |
POST | Admin | Disable admin mode |
/api/admin/impersonate/start |
POST | Admin Mode | Start impersonating a user |
/api/admin/impersonate/stop |
POST | Auth | Stop impersonation |
/api/admin/impersonate/status |
GET | Auth | Impersonation status |
/api/admin/users |
GET | Admin Mode | List users (with search) |
/api/admin/users/stats |
GET | Admin Mode | User statistics |
/api/admin/users/{id} |
GET | Admin Mode | Get user details |
/api/admin/users/{id} |
PATCH | Admin Mode | Update user (admin flag, active status) |
/api/admin/audit-logs |
GET | Admin Mode | List audit logs (with filtering/pagination) |
/api/admin/audit-logs/stats |
GET | Admin Mode | Audit log statistics |
/api/admin/tasks/dummy |
POST | Admin Mode | Enqueue the example background task |
The repo uses just for common tasks:
just test # unit tests (go test -short)
just test-integration # full integration suite (needs Node; Chrome/Redis optional)
just smoke # containerized end-to-end smoke test (Docker)Under the hood:
# Unit tests only (skips integration)
go test ./... -short
# Integration suite (scaffolds a project, builds it, runs serve + worker).
# Browser tests skip without Chrome; the worker test skips without Redis
# (set REDIS_ADDR to point at one).
go test ./integration/... -v
# Fully hermetic run in Docker (Go + Node + headless Chromium + Redis)
just smokeThe scaffolded frontend ships with Vitest tests; run them in
a generated project with just test (or npm test in frontend/).
- API Tests: Session management, OIDC flow (verified authenticated), protected endpoints, logout, health endpoint, first-user bootstrap
- Browser Tests: Real headless-Chrome login flow, asserting an authenticated session
- Scaffolding Tests: Project structure, module path handling, frontend deps/imports, full build, frontend Vitest run, mock OIDC end-to-end
- Background Tasks: Worker processes an enqueued task (against Redis)
- Smoke (CI + local): The whole path in one container — scaffold → build → serve → worker
# Build frontend
cd frontend && npm run build && cd ..
# Build Go binary (embeds frontend)
go build -o myapp ./cmd/myapp
# Run
./myapp serve -c config.ymlOr use GoReleaser:
goreleaser release --snapshot --clean┌─────────────────────────────────────────────────────────────┐
│ Browser │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Go HTTP Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Middleware │ │ Auth │ │ Frontend │ │
│ │ - Logging │ │ - OIDC │ │ - Dev: Vite proxy │ │
│ │ - Metrics │ │ - Session │ │ - Prod: Embedded │ │
│ │ - Recovery │ │ - Admin │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ Your API Handlers ││
│ └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ SQLite │ │ Redis │ │ OIDC │
│ (WAL) │ │ (tasks) │ │ Provider │
└──────────┘ └──────────┘ └──────────┘
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE for details.
Built with Claude and caffeine.