The full-stack Go framework that thinks in components.
Write Vue-style single-file components. Ship a single Go binary.
No Node. No JS bundler. No runtime. Just Go + HTMX.
Quickstart • Why GMX • The .gmx File • Features • Docs • Roadmap
GMX is a transpiler framework that compiles .gmx single-file components into production-ready Go applications with HTMX interactivity.
One file. Models, logic, templates, styles — all colocated. One command. A single, dependency-free binary.
todo.gmx → gmx build → ./todo (single binary, ~5MB, serves on :8080)
GMX doesn't hide Go or HTMX. It makes them work together with type safety, auto-generated routes, built-in security, and zero JavaScript.
# Install
go install github.com/btouchard/gmx/cmd/gmx@latest
# Create your first component
cat > todo.gmx << 'EOF'
<script>
model Task {
id: uuid @pk @default(uuid_v4)
title: string @min(3) @max(255)
done: bool @default(false)
}
service Database {
provider: "sqlite"
url: string @env("DATABASE_URL")
}
func toggleTask(id: uuid) error {
let task = try Task.find(id)
task.done = !task.done
try task.save()
return render(task)
}
</script>
<template>
<ul>
{{range .Tasks}}
<li id="task-{{.ID}}">
<button hx-patch="{{route `toggleTask`}}?id={{.ID}}"
hx-target="#task-{{.ID}}" hx-swap="outerHTML">
{{if .Done}}✓{{else}}○{{end}} {{.Title}}
</button>
</li>
{{end}}
</ul>
</template>
<style>
li { padding: 0.5rem; cursor: pointer; }
.done { text-decoration: line-through; opacity: 0.5; }
</style>
EOF
# Build & run
gmx build todo.gmx # → produces ./todo binary
DATABASE_URL="app.db" ./todo # → serves on :8080
# Or build + run in one step
DATABASE_URL="app.db" gmx run todo.gmxThat's it. You have a working CRUD app with HTMX reactivity, SQLite persistence, input validation, and CSRF protection. In one file.
Building modern web apps means choosing between two extremes:
| JS Frameworks (Next, Nuxt, SvelteKit) | Go Frameworks (Gin, Echo, Fiber) | |
|---|---|---|
| DX | Great (components, hot reload) | Verbose (scattered files) |
| Performance | Runtime overhead, hydration | Fast, but no component model |
| Deployment | Node runtime, Docker images | Single binary ✓ |
| Type safety | Partial (runtime errors) | Strong ✓ |
| Bundle | JS + CSS + sourcemaps | Just a binary |
GMX gives you Vue's developer experience with Go's production characteristics:
- Component colocation — Model, logic, template, style in one file
- Single binary output — No runtime, no Docker, just
scpand run - Type-safe HTMX — Auto-generated routes, validated parameters, no broken links
- Zero JavaScript — HTMX handles interactivity, Go handles everything else
- Built-in security — CSRF, XSS escaping, SQL injection prevention, all automatic
A .gmx file is a single-file component inspired by Vue's SFC format. Everything your feature needs lives in one place.
<script>
// ── Imports ──────────────────────────────────────
import TaskItem from "./components/TaskItem.gmx"
import { sendEmail } from "./services/mailer.gmx"
import "github.com/stripe/stripe-go" as Stripe
// ── Constants & Variables ────────────────────────
const MAX_TASKS = 100
let requestCount: int = 0
// ── Models (auto-generates DB schema + ORM) ─────
model Task {
id: uuid @pk @default(uuid_v4)
title: string @min(3) @max(255)
done: bool @default(false)
priority: int @min(1) @max(5) @default(3)
tenantId: uuid @scoped // ← auto multi-tenancy
author: User @relation(references: [id])
}
// ── Services (infra config, 12-factor) ──────────
service Database {
provider: "sqlite"
url: string @env("DATABASE_URL")
}
service Mailer {
provider: "smtp"
host: string @env("SMTP_HOST")
pass: string @env("SMTP_PASS")
func send(to: string, subject: string, body: string) error
}
// ── Handlers (auto-routed, type-checked) ────────
func createTask(title: string, priority: int) error {
if title == "" {
return error("Title cannot be empty")
}
const task = Task{title: title, priority: priority, done: false}
try task.save()
return render(task)
}
func toggleTask(id: uuid) error {
let task = try Task.find(id)
task.done = !task.done
try task.save()
return render(task)
}
func deleteTask(id: uuid) error {
let task = try Task.find(id)
try task.delete()
return nil
}
</script>
<template>
<form hx-post="{{route `createTask`}}" hx-target="#task-list" hx-swap="beforeend">
<input name="title" placeholder="What needs to be done?" required />
<button type="submit">Add</button>
</form>
<ul id="task-list">
{{range .Tasks}}
<li id="task-{{.ID}}" class="{{if .Done}}done{{end}}">
<button hx-patch="{{route `toggleTask`}}?id={{.ID}}"
hx-target="#task-{{.ID}}" hx-swap="outerHTML">
{{if .Done}}✓{{else}}○{{end}} {{.Title}}
</button>
<button hx-delete="{{route `deleteTask`}}?id={{.ID}}"
hx-target="#task-{{.ID}}" hx-swap="outerHTML"
hx-confirm="Delete this task?">×</button>
</li>
{{end}}
</ul>
</template>
<style>
form { display: flex; gap: 0.5rem; margin-bottom: 1rem; }
li { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; }
.done { text-decoration: line-through; opacity: 0.5; }
</style>| You write | GMX generates |
|---|---|
model Task { ... } |
Go struct + GORM tags + validation + ORM methods + SQL migrations |
func toggleTask(...) |
HTTP handler + route registration + param parsing + CSRF check |
{{route toggleTask}} |
Type-safe URL /api/toggleTask (compile error if function doesn't exist) |
@scoped on tenantId |
WHERE tenant_id = ? injected on every query, automatically |
@min(3) @max(255) |
Server-side validation before any DB operation |
<style> |
Scoped CSS embedded in the binary via go:embed |
import X from "Y.gmx" |
Recursive multi-file resolution, AST merging, template composition |
- Single-file components with
<script>,<template>,<style>sections - Import system — Vue-style default, destructured, and Go native imports
- Multi-file compilation with recursive dependency resolution and circular import detection
- Scoped CSS with automatic class prefixing
- Declarative models with type-safe annotations (
@pk,@unique,@email,@min,@max,@default,@relation) - Auto-generated ORM —
Task.find(id),Task.all(),.save(),.delete() - Multi-tenancy —
@scopedinjects tenant isolation on all queries - Database providers — SQLite & PostgreSQL via service configuration
- Typed route resolution —
{{route "funcName"}}validated at compile time - Auto handler generation — functions become HTTP endpoints with correct methods
- OOB swaps —
render(Task, SidebarCounter)for multi-target updates - Fragment rendering — handlers return HTML partials, not full pages
- CSRF protection — Double-submit cookies, auto-injected in forms and HTMX headers
- XSS prevention — Contextual auto-escaping via Go's
html/template - SQL injection — Parameterized queries only, no string concatenation
- Input validation — Model constraints enforced server-side before every operation
- UUID validation — Path parameters validated before reaching handlers
- Security headers — Middleware with CSP, X-Frame-Options, etc.
- Services — Database, SMTP, HTTP clients, S3 storage as typed declarations
- Environment config —
@env("VAR")with validation, 12-factor compliant - Dependency injection — Services auto-injected into handler context
- Go imports —
import "github.com/pkg" as Aliasmaps directly togo.mod
gmx build— Compile.gmxto a single Go binary (-ofor custom output path)gmx run— Build and execute immediately (pass args after--)gmx fmt— Format.gmxfiles with consistent indentation (-dfor diff mode)- Embedded assets — CSS, templates compiled in via
go:embed - ~5MB binaries — Go's static compilation, nothing extra
- Zero Docker needed —
scp binary server:/ && ./binary
GMX supports three import styles, all inside <script>:
// 1. Component import (like Vue)
// Imports the component's template, models, and styles
import TaskItem from "./components/TaskItem.gmx"
// 2. Destructured import (pick what you need)
// Cherry-pick functions, models, or services from another file
import { sendEmail, MailerConfig } from "./services/mailer.gmx"
// 3. Go native import (use any Go package)
// Adds to go.mod, available with alias in your script
import "github.com/stripe/stripe-go" as StripeImports are resolved recursively — if TaskItem.gmx imports Badge.gmx, it just works. Circular imports are detected at compile time.
GMX Script is a TypeScript-inspired syntax that transpiles to Go. It's intentionally small — not a new language, but a thin layer over Go with better ergonomics for web handlers.
| GMX Script | Generated Go |
|---|---|
let task = try Task.find(id) |
task, err := TaskFind(db, id); if err != nil { return err } |
try task.save() |
if err := TaskSave(db, task); if err != nil { return err } |
return render(task) |
return tmpl.ExecuteTemplate(w, "task", task) |
return error("Not found") |
return fmt.Errorf("Not found") |
let userId = ctx.User |
userId := ctx.User |
"Task: {t.title}" |
fmt.Sprintf("Task: %s", t.Title) |
Error handling uses try (unwrap-or-return), inspired by Rust/Swift. No more if err != nil boilerplate.
.gmx file
│
▼
┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ ┌──────────┐
│ Lexer │ → │ Parser │ → │ Resolver │ → │ Generator │ → │ go build │
│ (tokens) │ │ (AST) │ │ (imports) │ │ (Go code) │ │ (binary) │
└──────────┘ └──────────┘ └───────────┘ └───────────┘ └──────────┘
│ │
▼ ▼
Script Parser gen_models.go
(GMX Script → AST) gen_handlers.go
gen_template.go
gen_imports.go
gen_services.go
gen_vars.go
gen_helpers.go
gen_main.go
gen_components.go
The compiler is fully modular — each phase is independently testable with 91%+ test coverage.
| GMX | Templ + HTMX | Next.js | Laravel | |
|---|---|---|---|---|
| Single-file components | ✅ | ❌ | ✅ | ✅ (Blade) |
| Type-safe routes | ✅ | ❌ | ❌ | ❌ |
| Single binary | ✅ | ✅ | ❌ | ❌ |
| Zero JS needed | ✅ | ✅ | ❌ | ✅ (optional) |
| Auto multi-tenancy | ✅ | ❌ | ❌ | ❌ |
| Built-in CSRF | ✅ | Manual | ✅ | ✅ |
| Auto ORM from schema | ✅ | ❌ | Prisma | Eloquent |
| Component imports | ✅ | ❌ | ✅ | ✅ |
| No runtime deps | ✅ | ✅ | ❌ | ❌ |
| Learning curve | Low | Medium | High | Medium |
Todo app, SQLite, single machine, 20 rows.
| Stack | Read req/s | Write req/s | p99 read | RAM | JS bundle |
|---|---|---|---|---|---|
| GMX (Go+HTMX) | ~14 000 | ~315 | 1-2 ms | ~102 MB | 0 KB |
| Next.js (SSR) | ~2 000-4 000 | ~200-500 | 15-50 ms | ~150-300 MB | 80-200 KB |
| Rails (Hotwire) | ~800-1 500 | ~200-400 | 20-80 ms | ~150-250 MB | ~15 KB |
| Django (HTMX) | ~500-1 200 | ~150-350 | 30-100 ms | ~80-150 MB | 0 KB |
| Laravel (Livewire) | ~600-1 000 | ~150-300 | 25-80 ms | ~100-200 MB | ~30 KB |
| Phoenix (LiveView) | ~10 000-15 000 | ~3 000-5 000 | 2-5 ms | ~50-80 MB | ~15 KB |
Les valeurs des autres stacks sont des ordres de grandeur issus de benchmarks publics dans des conditions similaires (todo app, SQLite, single core). Le bottleneck write (~315 req/s) vient de SQLite (write lock global), pas de Go. Avec Postgres, ce chiffre monterait facilement a ~5 000+ req/s.
your-app/
├── app.gmx # Main component (entry point)
├── components/
│ ├── TaskItem.gmx # Reusable component
│ └── Navbar.gmx
├── services/
│ └── mailer.gmx # Shared service + functions
└── .env # Environment variables
gmx build app.gmx # → produces ./app binary
gmx build -o server app.gmx # → produces ./server binary
gmx run app.gmx # → build + run immediately
gmx fmt app.gmx components/*.gmx # → format files in placeFull documentation available at btouchard.github.io/gmx or locally:
pip install mkdocs-material
mkdocs serve
# → http://127.0.0.1:8000Guides: Getting Started, Components, Models, Script, Templates, Services, Security
Contributing: Architecture, AST Reference, Lexer & Parser, Generator, Script Transpiler, Testing
- Lexer with unicode, line/col tracking, all operators
- Section-aware parser (model, service, func, let/const, import)
- GMX Script transpiler (let, try, if/else, render, error, ctx)
- Code generator (models, handlers, templates, routes, main)
- Service infrastructure (SQLite, PostgreSQL, SMTP, HTTP)
- Security (CSRF, XSS, SQL injection, UUID validation, headers)
- Import system (Vue-style, destructured, Go native)
- Multi-file compilation with recursive resolution
- Scoped CSS
-
gmx dev— File watcher + live reload - Background tasks (
@async,@cron) - OOB swap generation (
render(A, B)→ concatenated HTML) - Tailwind JIT integration
-
gmx init— Project scaffolding - Source maps (GMX line → Go line)
GMX is open source and contributions are welcome.
git clone https://github.com/btouchard/gmx.git
cd gmx
go test ./... # Run all tests (~91% coverage)
go build -o gmx ./cmd/gmx # Build the compiler
# Usage
./gmx build app.gmx # Compile .gmx → binary
./gmx run app.gmx # Build + run immediately
./gmx fmt app.gmx # Format .gmx filesThe codebase is structured for clarity: internal/compiler/ contains the lexer, parser, resolver, script transpiler, and generator — each with comprehensive tests.
See CONTRIBUTING.md for architecture details.
Apache 2.0 — see LICENSE
The code generated by GMX belongs entirely to you. The Apache 2.0 license applies only to the GMX compiler itself.
Stop shipping JavaScript. Start shipping binaries.
Get started →