惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
V
Vulnerabilities – Threatpost
有赞技术团队
有赞技术团队
小众软件
小众软件
O
OpenAI News
C
Cyber Attacks, Cyber Crime and Cyber Security
I
Intezer
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
阮一峰的网络日志
阮一峰的网络日志
Hacker News: Ask HN
Hacker News: Ask HN
D
Docker
WordPress大学
WordPress大学
Security Archives - TechRepublic
Security Archives - TechRepublic
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
C
CERT Recently Published Vulnerability Notes
L
LINUX DO - 最新话题
Application and Cybersecurity Blog
Application and Cybersecurity Blog
M
MIT News - Artificial intelligence
Blog — PlanetScale
Blog — PlanetScale
S
Security @ Cisco Blogs
Cloudbric
Cloudbric
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
Hacker News - Newest:
Hacker News - Newest: "LLM"
G
Google Developers Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Hackread – Cybersecurity News, Data Breaches, AI and More
G
GRAHAM CLULEY
S
Schneier on Security
T
Tor Project blog
Spread Privacy
Spread Privacy
PCI Perspectives
PCI Perspectives
Microsoft Security Blog
Microsoft Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
F
Fortinet All Blogs
L
Lohrmann on Cybersecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
The Exploit Database - CXSecurity.com
TaoSecurity Blog
TaoSecurity Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
Threat Research - Cisco Blogs
T
Troy Hunt's Blog
罗磊的独立博客

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
GitHub - Lvcky-gg/Gjallarhorn
github.com b · 2026-06-27 · via Lobsters

A from-scratch web framework in Odin. No dependencies.

Gjallarhorn is the horn Heimdall sounds at the gates of Ásgarð. Here it's a small, honest web framework: a hand-rolled HTTP server, a router, an onion of middleware, a template engine, and an ORM that speaks PostgreSQL over a wire protocol written from scratch. No libpq, no third-party packages — just structs, runes, and the well of memory.

It is young and says so. Each module documents what it does, what its security checkpoint is, and what's deferred to a later phase. See Status & limitations for an unvarnished account of what works today.


Quickstart

You need the Odin compiler on your path. A database is optional — without one, the framework still runs and just prints the SQL it would migrate.

# 1. (optional) bring up Postgres for the ORM
docker compose up -d

# 2. run the sample app — serves on http://127.0.0.1:8091
odin run .

# 3. run the template-engine tests
odin test ./tests

If you skip step 1, leave dbname empty in main.odin. Migrations then print their DDL to stdout instead of executing, and the DB-backed routes return 503 — everything else (routing, middleware, static files, templates) works.

Try the running sample:

curl http://127.0.0.1:8091/sample/schema     # the SQL Mímir builds for the model
curl http://127.0.0.1:8091/sample/7          # a Sample row as JSON
curl http://127.0.0.1:8091/pages/hello.html  # a Loom-rendered template
curl http://127.0.0.1:8091/docs              # the static docs site

Install — use it in your own project

Odin has no central package manager: you install a package by putting its source where the compiler can import it. Gjallarhorn depends only on core: and base: (no third-party packages), so there is nothing else to fetch.

Option A — vendor the directory (simplest). Copy or clone just the framework package into your project next to your main.odin, then import it by folder name:

your-app/
├── main.odin
└── gjallarhorn/        # copy of this repo's gjallarhorn/ package directory
import gh "gjallarhorn"   // resolves to the ./gjallarhorn subdirectory

Option B — git submodule + a collection. Track the repo and point an Odin collection at it, so updates are a git pull away. The submodule checks out the whole repo, whose framework lives in its inner gjallarhorn/ package directory:

git submodule add https://github.com/lvcky-gg/gjallarhorn vendor/gjallarhorn
# framework package is now at vendor/gjallarhorn/gjallarhorn
import gh "shared:gjallarhorn"   // resolves to <collection-root>/gjallarhorn
# point the `shared` collection root at the repo checkout, so `shared:gjallarhorn`
# lands on its inner package directory
odin build . -collection:shared=vendor/gjallarhorn

Either way, build as usual. To turn on TLS (DB or HTTPS) add the opt-in flag — see TLS / HTTPS:

odin build .                      # plaintext; no OpenSSL dependency
odin build . -define:GJ_TLS=true  # links system libssl for TLS

Requirements: the Odin compiler on your path, and — only for a -define:GJ_TLS=true build — system OpenSSL (libssl/libcrypto).


A minimal app

package main

import gh "gjallarhorn"

User :: struct {
    id:   int    `db:"id,pk,auto"`,
    name: string `db:"name,notnull"`,
}

hello :: proc(b: ^gh.Bifrost) {
    name, _ := gh.param(b, "name")
    gh.json(b, 200, User{id = 1, name = name})
}

main :: proc() {
    app := gh.new(gh.Config{
        port    = 8091,
        db_type = .Postgres,
        postgres = gh.Postgres_Config{
            host = "127.0.0.1", port = 5432,
            user = "app", password = "secret", dbname = "gjallarhorn",
        },
    })

    // Middleware ("runes"), registered onion-style, outermost first.
    gh.rune(&app, gh.logger)
    gh.rune(&app, gh.cors)

    // Let Mímir remember the model; its table is auto-migrated at run().
    gh.remember(&app, User)

    // Routes.
    gh.get(&app, "/hello/:name", hello)

    // Mount a static dir and a template dir.
    gh.hail(&app, "/static", "./public")
    gh.hail(&app, "/pages", "./templates", page_context)

    gh.run(&app)
}

page_context :: proc(b: ^gh.Bifrost) -> gh.Warp {
    return gh.warp(
        {"title", "Hello"},
        {"items", gh.list("urd", "verdandi", "skuld", allocator = context.temp_allocator)},
        allocator = context.temp_allocator,
    )
}

The pieces

Odin is one-directory-one-package, so the whole framework lives in package gjallarhorn across several files. Each feature keeps its registration verb next to its logic.

File What it holds
app.odin App / Config / new
server.odin listen / accept (one thread per connection) / request parsing / keep-alive
router.odin routes, get/post/put/delete, path matching + dispatch
middleware.odin the Rune chain: rune, next, and built-in cors, logger
bifrost.odin the request/response object and its helpers
body.odin request-body decoders: bind_json, form, query/percent decoding
response.odin writing HTTP/1.1 responses
session.odin signed-cookie sessions + cookie / set_cookie
static.odin hail + traversal-safe file serving
loom.odin HTTP glue for Loom: render, html, directory mounts
loom/ Loom, the template engine (package loom)
mimir.odin Mímir, the ORM (writes and reads — scan hydrates rows into structs)
postgres.odin a from-scratch PostgreSQL v3 wire-protocol client (SCRAM auth, pooling)
tls.odin optional OpenSSL TLS for the DB connection and the HTTP server (opt-in)

Routing

Method verbs register routes; :name segments capture into params.

gh.get(&app, "/sample/:id", get_handler)
gh.post(&app, "/sample", create_handler)
gh.put(&app, "/sample/:id", update_handler)
gh.delete(&app, "/sample/:id", delete_handler)

Inside a handler, the Bifrost is your request and response:

get_handler :: proc(b: ^gh.Bifrost) {
    id, ok := gh.param_int(b, "id")
    if !ok {
        gh.text(b, 400, "id must be an integer")
        return
    }
    gh.json(b, 200, Sample{id = id, name = "thing"})
}

Literal routes should be registered before :param routes that could shadow them (/sample/schema before /sample/:id).

Reading request data. The Bifrost exposes every part of the request through small helpers; most return (value, ok) so a missing field is explicit:

id, ok    := gh.param(b, "id")          // path segment  /sample/:id
id, ok    := gh.param_int(b, "id")      // same, parsed to int (ok=false if NaN)
q, ok     := gh.query_param(b, "page")  // query string  ?page=2
ua, ok    := gh.header(b, "user-agent") // request header (keys are lower-cased)

Bodies are decoded on demand. bind_json unmarshals the body into a struct and writes a 400 for you on malformed input; form decodes an application/x-www-form-urlencoded body into a map:

create :: proc(b: ^gh.Bifrost) {
    payload: struct { name: string }
    if !gh.bind_json(b, &payload) { return } // 400 already written on bad JSON
    gh.json(b, 201, User{name = payload.name})
}

login :: proc(b: ^gh.Bifrost) {
    fields := gh.form(b)                      // map[string]string
    user, pass := fields["user"], fields["password"]
    // ...
}

The raw body is also on the Bifrost as b.body ([]u8) and b.body_text (string) if you need to decode it yourself. Bodies are framed by Content-Length and capped at Config.max_body (default 1 MiB), beyond which the server returns 413 before your handler runs.

Writing the response. text, json, and html set the status, content type, and body in one call; set_header adds a response header; not_found writes a 404. The first write wins — a second text/json on the same Bifrost is a no-op, so an early return after writing is safe.

Cookies. cookie(b, name) reads from the request; set_cookie queues a Set-Cookie (each call its own header line, so several cookies coexist):

sid, ok := gh.cookie(b, "session")
gh.set_cookie(b, "session", token, gh.Cookie_Options{
    http_only = true, secure = true, same_site = .Lax, max_age = 3600,
})
gh.set_cookie(b, "session", "", gh.Cookie_Options{max_age = 0}) // delete

max_age is a Maybe(int): omit it for a session cookie, 0 to expire now. Values are stored verbatim — encode any value carrying ;, ,, =, or whitespace yourself.

Sessions ride in a signed cookie — a string->string map the client holds, tamper-proofed with an HMAC-SHA256 tag keyed by Config.secret. The server keeps no state; a forged or edited cookie reads back as an empty session.

gh.session_set(b, "user", "freyja")   // re-signs the cookie
name, ok := gh.session_get(b, "user")
gh.session_clear(b)                    // empties + expires the cookie

Set Config.secret in production — when it's empty, sessions fall back to a fixed, public default key and new() warns at startup.

Middleware (Runes)

A Rune wraps the rest of the pipeline. Odin has no closures, so the remaining chain is threaded through the Bifrost rather than captured — call next(b) to run the next layer.

auth :: proc(b: ^gh.Bifrost, next: gh.Next) {
    // ...inspect the request, maybe short-circuit...
    next(b)  // or don't, to stop the chain
}

gh.rune(&app, auth)

Built-ins: logger (one line per request) and cors (permissive CORS + preflight OPTIONS short-circuit).

Mímir — the ORM

Your structs describe a shape; db: tags drive everything. Mímir remembers the shape and migrates it to a table at run().

Sample :: struct {
    id:   int    `db:"id,pk,auto"`,    // auto-assigned primary key
    name: string `db:"name,notnull"`,  // required text column
}

Tag flags: pk, auto, unique, notnull, a custom column name, or - to skip a field. The query verbs follow the well's vocabulary:

Verb SQL Meaning
carve CREATE TABLE carve a struct's shape into the well
offer INSERT offer a value to the well
recall SELECT recall rows (a Query you refine, then sql)
amend UPDATE amend a remembered row by primary key
forget DELETE make the well forget a row by primary key
w := gh.well(b)

q := gh.recall(w, Sample)
gh.whose(&q, "id = ?", 7)
gh.limit(&q, 1)
rows, ok := gh.query(w, gh.sql(&q))

Reading rows back into structs. query returns Pg_Rows (text cells); scan hydrates every row into a freshly allocated []T, and scan_one returns just the first row with an ok for the empty case. Columns map to fields by db: name (else the field name); a SQL NULL becomes the field's zero value.

rows, ok := gh.query(w, gh.sql(&q))
users := gh.scan(rows, User)            // []User
one, found := gh.scan_one(rows, User)   // (User, bool)

Supported field types: the int family, f32/f64, bool, and string.

Writes use the same query verb with offer/amend/forget, or exec when you don't need the returned rows:

gh.query(w, gh.offer(w, User{name = "freyja"}))          // INSERT
gh.query(w, gh.amend(w, User{id = 1, name = "renamed"}))  // UPDATE by pk
gh.query(w, gh.forget(w, User{id = 1}))                   // DELETE by pk

Transactions. tx checks out one pooled connection, wraps your closure in BEGIN/COMMIT, and rolls back if it returns false (or any statement fails). Every statement on the handed-in Well runs on that one connection:

ok := gh.tx(w, proc(w: gh.Well) -> bool {
    _, a := gh.query(w, gh.offer(w, User{name = "a"}))
    _, b := gh.query(w, gh.offer(w, User{name = "b"}))
    return a && b // either insert failing rolls back both
})

SQL injection is the checkpoint here: values never reach the SQL string. Every value is a bound parameter ($1.. for Postgres, ? otherwise).

Set db_type to .Postgres, .MySQL, or .SQLite. DDL is generated for all three; the live driver today is Postgres (see limitations).

Postgres — a hand-rolled wire client

postgres.odin implements the PostgreSQL v3 frontend/backend protocol directly over core:net: StartupMessage, the extended query flow (Parse / Bind / Describe / Execute / Sync), and RowDescription/DataRow parsing. Connections are pooled (Config.pool_size, default 4) and checked out per request.

Auth: trust, cleartext, MD5, and SCRAM-SHA-256 — the default for stock modern Postgres — so no pg_hba.conf downgrade is needed to connect to a default-configured server.

TLS (optional). Set Postgres_Config.sslmode to negotiate TLS before the startup handshake, so the password and all queries cross the wire encrypted:

postgres = gh.Postgres_Config{
    host = "db.example.com", port = 5432,
    user = "app", password = "secret", dbname = "gjallarhorn",
    sslmode = .Require,   // .Disable (default) / .Prefer / .Require / .Verify_Full
}
sslmode Behaviour
.Disable no TLS; cleartext (the default — unchanged behaviour)
.Prefer use TLS if the server offers it, else fall back to cleartext
.Require TLS mandatory; certificate not verified
.Verify_Full TLS mandatory; verify the cert chain + hostname against the system CA bundle

Any mode other than .Disable requires a TLS build — see TLS / HTTPS below. Without it, startup fails loudly rather than silently sending the password in the clear.

Loom — the template engine

A Jinja subset, pared to its load-bearing parts. The Norns weave fate at the well; here weave runs your data (the weft) through a template (the warp).

<h1>{{ title }}</h1>
<p>Hail, {{ user.name }}{% if user.admin %} <strong>(admin)</strong>{% endif %}.</p>

{% if items %}
<ol>
{% for item in items %}
  <li>#{{ loop.index }} — {{ item | upper }}{% if loop.last %} (last){% endif %}</li>
{% endfor %}
</ol>
{% else %}
<p>Nothing woven yet.</p>
{% endif %}

Supported: {{ expr }}, filter pipelines (upper, lower, trim, capitalize, length, default, join, first, last, safe, escape), {% if %} / {% elif %} / {% else %}, {% for x in xs %} with {% else %} for the empty case and a Jinja-style loop (index, index0, first, last, length), and {# comments #}. A - on either delimiter ({%- … -%}, {{- … -}}, {#- … -#}) trims adjacent whitespace, so tags don't leave stray newlines.

Template inheritance — a child names a base with {% extends "base.html" %} and overrides its named blocks; blocks it leaves alone keep the base's default:

<!-- base.html -->
<html><body>{% block content %}default{% endblock %}</body></html>

<!-- page.html -->
{% extends "base.html" %}
{% block content %}<h1>{{ title }}</h1>{% endblock %}

Bases resolve against the template's own mount dir, and {% extends %} chains (grandchild → child → base) — the most-derived override of a block wins.

Includes{% include "partial.html" %} renders another template inline with the current context (loop vars and all). Partials resolve their own inheritance, so an included file may itself {% extends %} a base.

Path traversal is the checkpoint here too: extends/include names are clamped to the mount dir (same clean-and-contain check as the static mounts), so {% include "../../etc/passwd" %} is refused.

Templates served from disk are parsed once and cached by path + mtime, so a hot page is lexed and parsed only on its first hit and re-parsed only when the file changes — the per-render cost drops to evaluation and output.

Typed rows render directly. warp_of reflects a struct (a scanned Mímir row, say) into a context keyed by field name — no hand-built map:

gh.render(b, "user.html", gh.warp_of(row))           // {{ name }}, {{ admin }}
gh.warp({"users", gh.value_of(rows)})                // a slice -> {% for u in users %}

Nested structs become nested contexts ({{ profile.city }}) and slices iterate. A loom:"alias" field tag renames the key for templates; loom:"-" hides it.

XSS is the checkpoint here: output is HTML-escaped by default. Safety rides with the value as it's evaluated, so it's decided per output — pipe through | safe to emit verbatim.

Static files

gh.hail(&app, "/static", "./public")              // raw files
gh.hail(&app, "/pages", "./templates", provider)  // files woven by Loom

Path traversal is the checkpoint here: a resolved path is cleaned and must stay inside the mount root, else 403.


TLS / HTTPS

TLS is opt-in at build time. Odin ships no TLS in core or vendor, so Gjallarhorn binds the system OpenSSL (libssl/libcrypto) — but the whole binding lives behind a compile flag, so a default build links no libssl and doesn't need OpenSSL installed at all:

odin run .                       # plaintext: no OpenSSL needed, nothing linked
odin run . -define:GJ_TLS=true   # links libssl; enables DB TLS + HTTPS

A TLS build needs OpenSSL's development library present at build time and the runtime library at run time (libssl + libcrypto, 1.1.x or 3.x). If you configure TLS but build without -define:GJ_TLS=true, startup fails loudly instead of silently falling back to an unencrypted connection.

HTTPS server. Point Config at a PEM certificate and key; the listener then serves https:// instead of http://:

app := gh.new(gh.Config{
    port     = 8443,
    tls_cert = "server.crt", // PEM certificate chain
    tls_key  = "server.key", // PEM private key
})
gh.run(&app) // logs: listening on https://127.0.0.1:8443
odin run . -define:GJ_TLS=true

For local testing, a self-signed pair is enough:

openssl req -new -x509 -days 365 -nodes \
  -out server.crt -keyout server.key \
  -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

curl --cacert server.crt https://localhost:8443/      # verifies the chain
curl -k https://localhost:8443/                        # or skip verification

Database TLS uses the same build flag — set Postgres_Config.sslmode (see Postgres). The DB client and HTTP server share one tls.odin module.

Scope: a single listener is HTTP or HTTPS (no dual-port and no HTTP→HTTPS redirect); .Verify_Full trusts the system CA bundle (no custom-CA option yet); certs load once at boot (no hot reload on renewal).


Lifecycle

A request crosses Bifrost in order: the socket loop in server.odin parses the request line and builds a Bifrost, the rune chain runs outermost-first via next, and when the chain is exhausted dispatch_route matches a route (or a static/template mount) and calls the handler, which writes the response back through the same Bifrost.


Status & limitations

Gjallarhorn has grown a coherent spine and several genuinely above-toy pieces: a from-scratch Postgres client (SCRAM auth, pooling, optional TLS), a real template engine with correct escaping and inheritance, full request parsing, and an ORM that reads typed rows back into structs. It is still young and not battle-tested for production — but the headline gaps the early drafts listed are now closed.

Working today: request headers, bodies (JSON + form) and query params; HTTP keep-alive; one-thread-per-connection concurrency; configurable bind address; per-request panic recovery; cookies and signed-cookie sessions; the ORM's full read/write/transaction path with struct hydration; SCRAM-SHA-256 auth; connection pooling; optional TLS on both the DB connection and the HTTP server; template inheritance, includes, whitespace control, the compiled-node cache, and direct struct rendering.

Known gaps, in rough order of impact:

  • Postgres-only in practice. MySQL and SQLite generate DDL but have no live driver yet, so query/exec only run against Postgres.
  • No auth-guard middleware or CSRF protection yet — the .ward in the sample is still a TODO. Sessions and cookies exist to build these on.
  • Templates have no {% macro %}. Inheritance, includes, and whitespace control are in; macros are not.
  • TLS is opt-in and depends on system OpenSSL (by design — a default build has no TLS and no libssl). .Verify_Full trusts only the system CA bundle, and certs are loaded once at boot.
  • No structured logging or CI yet (the built-in logger writes one plain line per request).

A full backlog with fix guides lives alongside this project (backlog.md). Contributions toward any of the above are the most useful place to start.


Project layout

.
├── gjallarhorn/        # the framework (package gjallarhorn)
├── sample/             # a small MVC example app
├── templates/          # Loom templates served at /pages
├── docs/               # the static docs site served at /docs
├── tests/              # Loom engine tests (odin test ./tests)
├── docker-compose.yml  # a local Postgres for the ORM
└── main.odin           # wires the sample app together