v0.1.0

The backend language
for the htmx era

Declarative backend programming. SQL as first-class citizen, HTML as native output, htmx-aware. 27 keywords. Single binary. Zero config.

app.kilnx
A complete app in one file
config
  database: env DATABASE_URL default "sqlite://app.db"
  port: env PORT default 8080
  secret: env SECRET_KEY required

model user
  name: text required
  email: email unique
  password: password required
  role: option [admin, user] default user
  created: timestamp auto

model task
  title: text required
  done: bool default false
  owner: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks requires auth
  query tasks: SELECT id, title, done FROM task
               WHERE owner = :current_user.id
               ORDER BY created DESC
               paginate 20
  html
    <h1>My Tasks</h1>
    {{each tasks}}
    <div>
      <span>{title}</span>
      <button hx-post="/tasks/{id}/toggle">Toggle</button>
    </div>
    {{end}}

action /tasks/create method POST requires auth
  validate task
  query: INSERT INTO task (title, owner) VALUES (:title, :current_user.id)
  redirect /tasks

Same app. Less everything.

A task manager with auth, CRUD, and real-time updates. One requires a stack. The other requires Kilnx.

Traditional Stack
Express + Prisma + EJS + Passport ~12 files, ~480 lines
package.json
"express", "prisma", "@prisma/client",
"ejs", "passport", "passport-local",
"express-session", "csurf", "bcrypt",
"express-validator", "connect-flash"
// 11 dependencies

schema.prisma
model Task {
  id        Int      @id @default(autoincrement())
  title     String
  done      Boolean  @default(false)
  createdAt DateTime @default(now())
  userId    Int
  user      User     @relation(...)
}

routes/tasks.js
const router = express.Router()
router.get('/', ensureAuth, async (req, res) => {
  const tasks = await prisma.task.findMany({
    where: { userId: req.user.id },
    orderBy: { createdAt: 'desc' }
  })
  res.render('tasks/index', { tasks })
})
// + POST, PUT, DELETE handlers
// + validation middleware
// + error handling

// + views/tasks/index.ejs
// + views/layout.ejs
// + config/passport.js
// + middleware/auth.js
// + middleware/csrf.js
// + prisma/migrations/...
// + app.js (server setup)
Kilnx
app.kilnx 1 file, 40 lines
config
  database: env DATABASE_URL default "sqlite://app.db"
  port: 8080
  secret: env SECRET_KEY required

model task
  title: text required
  done: bool default false
  owner: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /tasks

page /tasks requires auth
  query tasks: SELECT id, title, done FROM task
               WHERE owner = :current_user.id
               ORDER BY created DESC
               paginate 20
  html
    <h1>My Tasks</h1>
    {{each tasks}}
    <div>
      <span>{title}</span>
      <button hx-post="/tasks/{id}/toggle">Toggle</button>
    </div>
    {{end}}

action /tasks/create method POST requires auth
  validate task
  query: INSERT INTO task (title, owner)
         VALUES (:title, :current_user.id)
  redirect /tasks

Dependencies

11 0

Files

12+ 1

Lines of code

~480 40

Config files

5 0

Everything you need,
nothing you don't

27 keywords replace an entire stack. Models, pages, actions, auth, jobs, emails, websockets, API endpoints.

SQL First-Class

No ORM, no query builder. Write SQL inline with parameter binding. JOINs, aggregates, subqueries. Your database is a feature, not an afterthought.

HTML Native Output

HTML is the return type. Pages, fragments, and layouts compose naturally with {{each}}, {{if}}, and {field} interpolation. No template engine bolted on.

htmx-Aware

Fragments, SSE streams, WebSockets, and broadcast are built-in keywords. Partial HTML updates without writing a single line of JavaScript.

Single Binary

Compiles to one ~15MB executable with SQLite, bcrypt, and htmx.js embedded. No runtime, no dependencies, no node_modules. Copy and run.

Security by Default

CSRF tokens auto-injected in forms. SQL injection prevented by parameterized queries. XSS blocked by auto-escaping. Bcrypt password hashing built-in.

Zero Config

No framework to choose, no ORM to configure, no template engine to wire up. Create a .kilnx file, run it. One file can be a complete app.

From Hello World to production CRM

These are real, runnable examples. Not pseudocode. Copy, paste, kilnx run app.kilnx, done.

hello.kilnx
2 lines
page /
  "Hello World"

One useful line

No imports, no boilerplate, no configuration. A running HTTP server with htmx auto-linked.

$ kilnx run hello.kilnx
Server running on http://localhost:8080
blog.kilnx
94 lines. Auth, CRUD, pagination, tests. Everything.
config
  database: env DATABASE_URL default "sqlite://blog.db"
  port: env PORT default 8080
  secret: env SECRET_KEY default "dev-secret"

model user
  name: text required min 2 max 100
  email: email unique
  password: password required
  role: option [admin, author] default author
  created: timestamp auto

model post
  title: text required min 5
  body: richtext required
  status: option [draft, published] default draft
  author: user required
  created: timestamp auto

auth
  table: user
  identity: email
  password: password
  login: /login
  after login: /posts

page /
  query posts: SELECT p.title, p.created, u.name as author
               FROM post p
               LEFT JOIN user u ON u.id = p.author
               WHERE p.status = 'published'
               ORDER BY p.created DESC
               paginate 10
  html
    <h1>Blog</h1>
    {{each posts}}
    <article>
      <h2>{title}</h2>
      <span>by {author} on {created}</span>
    </article>
    {{end}}

page /posts requires auth
  query posts: SELECT id, title, status, created FROM post
               WHERE author = :current_user.id ORDER BY created DESC
  html
    <h1>My Posts</h1>
    <a href="/posts/new">New Post</a>
    {{each posts}}
    <div>
      <a href="/posts/{id}">{title}</a>
      <span>{status}</span>
    </div>
    {{end}}

action /posts/create method POST requires auth
  validate
    title: required min 5
    body: required
  query: INSERT INTO post (title, body, author, status)
         VALUES (:title, :body, :current_user.id, 'published')
  redirect /posts

action /posts/:id/delete method POST requires auth
  query: DELETE FROM post WHERE id = :id AND author = :current_user.id
  redirect /posts

fragment /posts/:id/preview
  query post: SELECT title, body, created FROM post WHERE id = :id
  html
    <article>
      <h2>{post.title}</h2>
      <p>{post.body}</p>
      <time>{post.created}</time>
    </article>

test "homepage loads"
  visit /
  expect page / contains "Blog"

test "create post requires auth"
  visit /posts/new
  expect page /login
crm/app.kilnx
~800 lines. Contacts, companies, deals, dashboards.
# Models with relationships
model company
  name: text required min 2 max 100
  industry: text
  website: text
  created_at: timestamp auto

model contact
  name: text required min 2 max 100
  email: email unique
  phone: phone
  role: text
  company: company required
  created_at: timestamp auto

model deal
  title: text required
  value: float required
  stage: option [lead, proposal, negotiation, closed_won, closed_lost] default lead
  contact: contact required
  notes: richtext
  created_at: timestamp auto

# Dashboard with aggregate queries
page / layout main title "Dashboard"
  query stats: SELECT
    (SELECT COUNT(*) FROM contact) as contacts,
    (SELECT COUNT(*) FROM company) as companies,
    (SELECT COUNT(*) FROM deal WHERE stage NOT IN ('closed_won','closed_lost')) as active_deals,
    (SELECT COALESCE(SUM(value),0) FROM deal WHERE stage = 'closed_won') as revenue
  query recent_deals: SELECT d.title, d.value, d.stage, c.name as contact
    FROM deal d LEFT JOIN contact c ON c.id = d.contact
    ORDER BY d.created_at DESC LIMIT 5
  html
    <div class="grid grid-cols-4 gap-4">
      <div class="stat-card">{stats.contacts} Contacts</div>
      <div class="stat-card">{stats.companies} Companies</div>
      <div class="stat-card">{stats.active_deals} Active Deals</div>
      <div class="stat-card">${stats.revenue} Revenue</div>
    </div>
    {{each recent_deals}}
    <tr>
      <td>{title}</td>
      <td>{contact}</td>
      <td>${value}</td>
      <td><span class="badge-{stage}">{stage}</span></td>
    </tr>
    {{end}}

# CRUD actions with validation and htmx
action /contacts/create method POST
  validate
    name: required min 2
    email: required, is email
  query: INSERT INTO contact (name, email, phone, role, company)
         VALUES (:name, :email, :phone, :role, :company)
  redirect /contacts

action /contacts/:id/delete method POST
  query: DELETE FROM contact WHERE id = :id
  redirect /contacts

The full CRM example is ~800 lines and includes layout with Tailwind CSS, sidebar navigation, deal pipeline, search, and responsive design. See the complete source on GitHub.

27 keywords

Python has 35 keywords and does none of this without importing libraries. JavaScript has 64. Java has 67. Kilnx has 27 and delivers a complete web app from database to browser.

kilnx explain
$ kilnx explain

Click a keyword to explore the language.

Less moving parts, same result

For the 80% use case (forms, dashboards, CRUDs with auth), Kilnx does more with less.

Kilnx Django Rails Next.js Phoenix
Dependencies 0 50+ 30+ 100+ 20+
Blog with auth (LOC) 94 ~500 ~400 ~1000 ~350
Compiled binary Yes (~15MB) No No No Yes (BEAM)
Built-in auth 6 lines Yes Devise gem No phx.gen.auth
Built-in jobs Yes Celery Sidekiq No Oban
Built-in testing Same file Django test RSpec Jest/Vitest ExUnit
Realtime (SSE/WS) Built-in Channels ActionCable Manual LiveView
Learning time ~1 hour ~40 hours ~40 hours ~30 hours ~50 hours

11 design
principles

htmx completed HTML. Kilnx completes htmx. Every design decision serves a single goal: make the backend as simple as the problem demands, not as complex as the tools allow.

"The kiln metaphor: raw declarations go in, a solid binary comes out. The X suffix creates visual kinship with htmx."
00The complexity is the tool's fault, not the problem's
01Zero decisions before the first useful line
02SQL is first-class citizen
03HTML is the native output
04Declarative first, imperative when necessary
05One file can be a complete app
06The binary is the deploy
07Fragments are first-class
08Security is default, not opt-in
09Zero dependencies for the user
10htmx awareness without htmx coupling

From zero to running in 60 seconds

1

Install

$ go install github.com/kilnx-org/kilnx@latest
2

Write

page /
  "Hello World"
3

Run

$ kilnx run app.kilnx
Server running on http://localhost:8080

CLI Commands

kilnx run <file>

Development server with hot reload

kilnx build <file> -o myapp

Compile to standalone binary

kilnx test <file>

Run declarative tests

kilnx check <file>

Static analysis and security scan

kilnx migrate <file>

Apply database migrations from models

kilnx lsp

Language server for VS Code and Neovim

Deploy anywhere

Binary
$ kilnx build app.kilnx -o server
$ scp server you@host:~/
$ ssh you@host "./server"
Docker
FROM ghcr.io/kilnx-org/kilnx AS build
COPY app.kilnx .
RUN kilnx build app.kilnx -o /server
FROM alpine:3.21
COPY --from=build /server /usr/local/bin/
CMD ["server"]
Railway / Fly.io / Render
# Just push. PORT is auto-detected.
$ git push railway main
$ flyctl deploy
$ render deploy
Deploy on Railway

Ready to build?

Star the repo, join the community, or start building right now.

$ go install github.com/kilnx-org/kilnx@latest