Routing built into the language. Persistent DB connections. WebSocket, SSE, parallel(), jobs::, template:: — all in a single binary. No framework to set up — just write.
app::set("db", db::connect("mysql://root:@127.0.0.1/shop")) # REST — 3 queries in parallel, capture-free DB access (app::db) route("GET", "/api/dashboard", function() { $ch = channel(3) parallel(function() use ($ch) { send($ch, db::col(app::db(), "SELECT count(*) FROM orders", [])) }) parallel(function() use ($ch) { send($ch, db::col(app::db(), "SELECT sum(total) FROM orders", [])) }) parallel(function() use ($ch) { send($ch, db::col(app::db(), "SELECT count(*) FROM users", [])) }) response::json(["orders" => receive($ch), "revenue" => receive($ch), "users" => receive($ch)]) }) # SSE — live notification stream route("SSE", "/events", function($sse) { $id = timer::every(3000, function() use ($sse) { sse::send($sse, json::encode(["t" => date::now()]), "tick") }) sse::on($sse, "close", function() use ($id) { timer::cancel($id) }) })
LOOK is a scripting language designed for server-side web development. Routing, DB, concurrency, real-time connections, background jobs and a template engine — all inside a single binary, with zero dependencies.
The app starts once. DB connection, route registrations, setup code — it all stays in memory.
Every HTTP request only runs dispatch_routes().
32 workers, 4.7 million requests, 10 minutes — 0 errors, 8 MB RAM.
route("GET", "/product/{id}", fn) — this isn't a library call, it's the core of the language.
No router to set up, no config, no middleware stack.
REST, WebSocket, SSE — all with the same route() syntax.
Lightweight concurrency, in a web scripting language. Run 3 DB queries at once
and collect the results with receive() — total time is max(t₁, t₂, t₃).
No extra runtime, no extra library, nothing new to learn for concurrency.
route("WS", "/chat", fn) — WebSocket, fully RFC 6455 compliant.
route("SSE", "/events", fn) — Server-Sent Events, together with timer::every().
500 concurrent WebSocket connections, 72 hours, 0 memory leaks.
Not a framework — a language. If any of the needs below is yours, LOOK is a great fit.
You love the ease of shared hosting / Plesk but want WebSocket, SSE and
concurrency. Drop the file, it runs — plus real-time connections
and parallel().
Routing, DB (MySQL/SQLite/PostgreSQL), validation, cache, JWT — all built
into the language. No composer/npm dependency tree, one binary.
Copy one file to a VPS, run it with systemd/Plesk. No separate runtime, no package manager, no surprises. 28 MB RAM, steady across 1M requests.
Built-in SMTP + IMAP server — no Postfix/Dovecot needed. Build a secure mail system, e-commerce notifications, a blog with LOOK. How? ↓
look.codlook.com · AlmaLinux 8 · MariaDB · measured on a direct port, 64 workers.
All tests were measured with ab on the live look.codlook.com
environment. Throughput is steady from c=1,000 to c=20,000 (~7.5–10k) — the bottleneck is CPU;
kernel walls (backlog, syn_backlog) were cleared with tuning. The TLS chain (nginx→FCGI) does ~1,100–1,240 RPS;
the difference comes from double TLS termination, not LOOK. DB-bound endpoints are limited by MySQL RTT.
Zero dependencies, every line tested. Passed fuzzing, ThreadSanitizer and data-integrity tests.
| Topic | LOOK protection |
|---|---|
| SQL injection | db::query($c, sql, [params]) — auto-parameterised ?, driver-correct escaping |
| DoS — huge body | LOOK_MAX_BODY_SIZE (10 MB) exceeded → 413; unbounded-body memory exhaustion prevented |
| Request smuggling | Content-Length + Transfer-Encoding together → 400 (RFC 7230 §3.3.3) |
| Malformed header | Malformed Content-Length → 400 (worker doesn't crash); full RFC 7230 chunked TE support |
| Slowloris / slow requests | Read/write timeout (30s) + kernel backlog 16384 + syn_backlog tuning |
| Session security | 128-bit /dev/urandom token; cookie HttpOnly + Secure + SameSite |
| Password storage | auth::hash / verify — PBKDF2-SHA256, in the core |
| Upload security | Magic-byte type validation (blocks extension spoofing) + SHA-256 |
| JSON stack overflow | Deeply nested JSON body → recursion depth limit (JSON_MAX_DEPTH) — stack overflow prevented |
| Protocol parser DoS | RESP2/HTTP/PG/SMTP: unguarded number conversion + unbounded allocation closed (a malformed server response can't crash a worker) |
| Arithmetic UB | Signed-integer overflow, shift and unary-minus undefined behaviour eliminated (promote to float on overflow) — UBSan clean |
Before every build, 9 regression tests + automated ASan/UBSan fuzzing and ThreadSanitizer run in CI
(.github/workflows/security.yml). Memory: RSS steady over 1.75M requests — no leak.
Most scripting languages send mail; LOOK serves it. A built-in SMTP (RFC 5321) and IMAP4rev1 (RFC 3501 + IDLE) server — no Postfix/Dovecot needed. You can build a fully secure mail system end to end with LOOK.
Thunderbird, Roundcube, Apple Mail and mobile clients connect directly — list, read, write, search, receive live notifications. All over TLS.
Write directly — nothing to install, nothing new to learn.
route("GET", "/product/{id}", fn) — REST, WS, SSE, same syntax. URL params parsed automatically.
fn($x) => $x * 2 — a function alias + expression-bodied arrow with implicit return. route("GET","/ping", fn() => response::json(["ok"=>true])). Classic function works too.
No more passing a shared connection to every route with use ($conn). app::set("db", $conn) once at setup → routes use app::db() with no capture. The explicit-capture philosophy is preserved.
Zero dependency, wire protocol implemented directly. Same API: db::query / db::exec / db::col. Transactions: db::begin / commit / rollback.
The LOOK task model. Fan-out, pipeline, producer-consumer — over channels. No blocking, no callbacks. Monitor with parallel::active() / limit() / at_capacity().
route("WS", "/chat", fn) — RFC 6455, SHA-1+Base64 handshake. ws::broadcast() to all connected clients.
route("SSE", "/events", fn) — Server-Sent Events. Periodic push with timer::every(3000, fn).
SQLite-backed durable queue. jobs::push / worker / run. Delayed jobs, retry, dead-letter. Not lost when the process exits.
In-memory, thread-safe, named queue. Passing data between requests. queue::push / pop / peek / size.
TTL-aware, thread-safe singleton. Shared across all workers with warm start. cache::get / set / has / flush.
Layout inheritance, partial includes, {#each} loops, {#if} conditions. Svelte-like — no runtime, server render only.
AST → register-based bytecode → switch dispatch. 7.8× faster than the interpreter on compute-heavy workloads.
Mailgun, SendGrid, Postmark supported. mail::send / send_html. Choose the provider with the MAIL_PROVIDER env var.
SHA-256, HMAC-SHA256, RS256, UUID v4, timing-safe compare — zero dependency. Sign and verify HS256/RS256 tokens with the official pkg/jwt package.
Two-layer protection: per-IP burst tolerance + global botnet cap. LOOK_RATE_LIMIT_RPM, LOOK_RATE_LIMIT_BURST, LOOK_RATE_LIMIT_GLOBAL_RPM. Your LOOK code doesn't change — it's at the runtime level.
Outbound e-mail is automatically signed with RSA-SHA256 (LOOK_SMTP_DKIM_*). Port 25 relay is blocked — authenticated SMTP only (587/465).
Zero-dependency MTA (25/587) + IMAP4rev1 server (143/993). SMTP delivers to Maildir, IMAP serves it to Thunderbird/Roundcube. STARTTLS, SEARCH, live IDLE push. Same mail_users pbkdf2 identity. No Dovecot/Postfix required.
Structured error objects with error::new($code, $message). error::is($e), error::code($e), error::message($e). No use required — always on.
GET, POST, PUT, PATCH, DELETE. JSON body, custom headers, timeout. TLS included. Fan-out parallel API calls with parallel().
lk install github.com/codlook/look-packages/... — JWT, payments and more. Explore the Codlook-approved ecosystem at packages.codlook.com. No registry, no config.
look test — 10 assertions, before_each/after_each. look repl — interactive. Install packages from GitHub with look install github.com/user/repo.
$conn = db::connect("mysql://root:@127.0.0.1/blog") use auth use validator use cache function admin_check() { session::start() if (session::get("admin_id") == null) { response::error(401, "Unauthorized") return false } return true } route("GET", "/api/posts", function() use ($conn) { # 5-minute cache — don't hit the DB for the same data over and over $data = cache::get("posts") if ($data == null) { $data = db::query($conn, "SELECT * FROM posts ORDER BY id DESC", []) cache::set("posts", $data, 300) } response::json(["ok" => true, "data" => $data]) }) route("POST", "/api/posts", function() use ($conn) { if (!admin_check()) { return } $v = validator::check(request::json(), [ "title" => ["required", "min:3", "max:200"], "content" => ["required", "min:10"] ]) if (!$v["ok"]) { response::json($v, 422) return } $data = request::json() db::exec($conn, "INSERT INTO posts (title,content) VALUES (?,?)", [$data["title"], $data["content"]]) cache::delete("posts") # cache invalidate response::json(["ok" => true, "id" => db::last_id($conn)]) })
# Hub: the shared broadcast channel for all connections (setup phase) $hub = channel() route("WS", "/chat", function($ws) use ($hub) { # One thread per connection: listen on the hub → write to this client parallel(function() use ($ws, $hub) { while (true) { $msg = receive($hub) if ($msg == null) { break } ws::send($ws, $msg) } }) # On incoming message, push to the hub → every client hears it ws::on($ws, "message", function($data) use ($hub) { $p = json::decode($data) send($hub, json::encode(["from" => request::ip(), "msg" => $p["msg"]])) }) ws::on($ws, "close", function() { log::info("Connection closed — active: " . ws::clients()) }) }) # Keepalive ping — to all clients every 30 seconds timer::every(30000, function() { ws::broadcast(json::encode(["type" => "ping"])) })
$conn = db::connect("mysql://root:@127.0.0.1/shop") route("GET", "/panel/summary", function() use ($conn) { $ch = channel(3) # waiting for 3 results parallel(function() use ($ch, $conn) { send($ch, db::col($conn, "SELECT count(*) FROM orders", [])) }) parallel(function() use ($ch, $conn) { send($ch, db::col($conn, "SELECT sum(total) FROM orders WHERE status='paid'", [])) }) parallel(function() use ($ch, $conn) { send($ch, db::col($conn, "SELECT count(*) FROM users WHERE active=1", [])) }) # Won't continue until all parallel jobs finish $orders = receive($ch) $revenue = receive($ch) $users = receive($ch) response::json([ "orders" => $orders, "revenue" => $revenue, "users" => $users ]) })
$conn = db::connect("mysql://root:@127.0.0.1/shop") # SSE: once the browser connects, a continuous data stream is pushed route("SSE", "/live/orders", function($sse) use ($conn) { # Check for new orders every 2 seconds $tick = timer::every(2000, function() use ($sse, $conn) { $new = db::query($conn, "SELECT * FROM orders WHERE created_at > date_sub(now(), interval 5 second)", []) if (count($new) > 0) { $ok = sse::send($sse, json::encode($new), "new_order") if (!$ok) { timer::cancel($tick) } # connection closed } }) # 30s keepalive — so the browser doesn't time out $ping = timer::every(30000, function() use ($sse) { sse::send($sse, "keepalive", "ping") }) sse::on($sse, "close", function() use ($tick, $ping) { timer::cancel($tick) timer::cancel($ping) log::info("SSE closed — active: " . sse::clients()) }) })
use jobs use mail # Route: respond instantly (1ms), enqueue the work route("POST", "/signup", function() use ($conn) { $data = request::json() db::exec($conn, "INSERT INTO users (email,name) VALUES (?,?)", [$data["email"], $data["name"]]) # max 3 attempts, process now (delay_sec=0) jobs::push("welcome_email", ["to" => $data["email"], "name" => $data["name"]], 3, 0) # reminder email after 10 minutes jobs::push("reminder_email", ["to" => $data["email"]], 3, 600) response::json(["ok" => true]) }) # worker.lk — separate process (runs as a systemd service) jobs::recover("welcome_email") # crash recovery jobs::worker("welcome_email", function($job) { $r = mail::send($job["payload"]["to"], "Welcome!", "Hi " . $job["payload"]["name"] . ", your account is ready.") return $r["ok"] # false → retry (max 3), then dead-letter }) jobs::run(5000) # poll every 5 seconds
use template # Struct — Go style, no methods, data only struct Post { id title content date likes: 0 published: true } $tpl = env("VIEWS_DIR", "/app/views") route("GET", "/blog", function() use ($conn, $tpl) { $rows = db::query($conn, "SELECT * FROM posts WHERE published=1", []) print(template::render($tpl . "/blog/index", [ "title" => "Blog", "posts" => $rows ])) }) # views/blog/index.html — server-side, no JS # {#extends "views/layout/base"} # {#block "content"} # {#each $posts as $p} # <article> # <h2>{$p.title}</h2> # <p>{$p.date} · {$p.likes} likes</p> # <div>{!$p.content}</div> # </article> # {/each} # {#empty}<p>No posts yet.</p>{/each} # {/block}
# Terminal: look install github.com/Codlook/look-packages use "pkg/jwt/jwt.lk" use auth use crypto $conn = db::connect(env("DB_DSN", "mysql://root:@127.0.0.1/app")) # Login — issue a token route("POST", "/api/login", function() use ($conn) { $email = request::post("email") ?? "" $password = request::post("password") ?? "" $rows = db::query($conn, "SELECT * FROM users WHERE email=?", [$email]) if (count($rows) == 0 || !auth::verify($password, $rows[0]["password_hash"])) { response::error(401, "Invalid credentials") return } $token = jwt_sign( ["user_id" => $rows[0]["id"], "role" => $rows[0]["role"]], env("JWT_SECRET", ""), ["exp" => 86400] # 24 hours ) response::json(["ok" => true, "token" => $token]) }) # Protected endpoint — Bearer token required function auth_required() { $header = request::header("Authorization") ?? "" $token = string::substr($header, 7) # strip "Bearer " $p = jwt_verify($token, env("JWT_SECRET", "")) if ($p == null) { response::json(["error" => "Invalid token"], 401) return null } return $p } route("GET", "/api/profile", function() use ($conn) { $user = auth_required() if ($user == null) { return } $rows = db::query($conn, "SELECT id,name,email FROM users WHERE id=?", [$user["user_id"]]) response::json(["ok" => true, "data" => $rows[0]]) }) # Webhook verification — HMAC-SHA256 timing-safe route("POST", "/webhook/payment", function() { $payload = request::body() $given = request::header("X-Signature") ?? "" $expected = "sha256=" . crypto::hmac_sha256($payload, env("WEBHOOK_SECRET", "")) if (!crypto::constant_compare($given, $expected)) { response::status(401) return } # Signature valid — process the payment... response::json(["ok" => true]) })
Write and run LOOK code in the browser — no install. It's in the works — once ready, a live editor will sit right here.
For now: download and run it in seconds with lk --mode http.
Login and data fetching — one file, no framework, no surprises.
| Topic | How LOOK does it |
|---|---|
| DB connection | Warm start — opened once, stays alive across every request |
| Request data | No globals — request:: / session:: namespaces |
| Routing | Built into the language — route("GET", "/path/{id}", fn) |
| SQL injection protection | db::query auto-parameterised (? placeholder) |
| Concurrency | parallel() + channel() inside the language |
| WebSocket / SSE | route("WS", ...) — part of the core |
| Performance (router) | Bytecode VM — 10,000+ RPS on a direct port, single core |
Every new feature must serve these three principles — otherwise it doesn't make it into the language.
use() declares exactly what comes in.
No hidden globals, no surprises.
Every component of LOOK is written in C++23 with zero dependencies.
Precedence-aware parser, full operator set, string interpolation, error positions via SourceLocation.
AST → 3-address register bytecode → switch dispatch. 7.8× faster on compute workloads. Fallback: tree-walk interpreter.
--workers N, per-request interpreter copy, per-DSN connection pool. Hot reload: zero downtime.
epoll (Linux), IOCP (Windows). RFC 6455 WS, SSE frame codec. Unlimited concurrent connections.
Wire protocol from scratch. No driver, no ORM. Same db:: API, different DSN.
Apache/nginx FastCGI, standalone HTTP, CGI fallback. One binary, three modes.
Choose your platform:
One package for Ubuntu/Debian + AlmaLinux/Rocky/RHEL — the same portable binary. From GitHub Releases:
unzip look-lang-linux-1.0.0.zip -d look && cd look
Installs the binary to /usr/local/bin, creates a sample app + systemd service (port 9000), starts it and verifies.
sudo bash install.sh
curl http://127.0.0.1:9000/ · systemctl status look
Prefer to manage it with dnf on AlmaLinux/RHEL: dnf install look-lang-1.0.0-1.el8.x86_64.rpm (then dnf update look-lang).
If XAMPP is already installed, skip this step.
Extract look-lang-xampp-1.0.0.zip; it copies lk-cgi.exe to cgi-bin, patches httpd.conf for .lk, and creates a sample test.lk.
Expand-Archive look-lang-xampp-1.0.0.zip look; cd look; .\install.ps1
Custom path: .\install.ps1 -XamppDir "D:\xampp" · Uninstall: .\uninstall.ps1
XAMPP Control Panel → Apache → Restart. Open http://localhost/test.lk — it's running.
Plesk Panel → Server Management → Terminal
If you prefer SSH: ssh root@server_ip
plesk bin extension --uninstall look-lang 2>/dev/null; wget -O /tmp/look-lang-plesk-1.0.0.zip "https://github.com/codlook/look/releases/download/v1.0/look-lang-plesk-1.0.0.zip" && plesk bin extension --install /tmp/look-lang-plesk-1.0.0.zip
You'll see "The extension was successfully installed."
Plesk Panel → Extensions → My Extensions → LOOK Language → Open
Applications → Add New Domain → pick a domain (script path and port fill in automatically) → Add & Start
After a few seconds it shows Running in the list. The systemd service + Apache proxy are created automatically.
Use Edit Code to edit index.lk in the browser → Save & Redeploy — the file is written and the service restarts.
Full workspace: live system dashboard, in-browser code editor, per-domain monitor (CPU/RAM/PID/connections), journalctl log viewer.
On a fresh server, one root command is needed to grant sudo access (see docs).
Docker is used to build the LOOK binaries. Separate Dockerfiles are provided for Ubuntu 24.04 and AlmaLinux 8.
docker build -f Dockerfile.linux-build -t look-builder .
docker run --rm -v "${PWD}:/src" look-builder sh -c "cd /src/build-linux && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j4"
cpp/build-linux/look-fcgi # ready binary
For AlmaLinux 8 (Plesk) use Dockerfile.almalinux8-build — because of the glibc difference, the Ubuntu binary won't run on AlmaLinux.
v1.0.0 · Apache 2.0 · Stable release for every platform.
One self-contained package. sudo bash install.sh → binary + systemd service. The same portable binary runs on Ubuntu/Debian and AlmaLinux/Rocky/RHEL.
dnf-managed install. dnf install look-lang-…rpm → then update with dnf update look-lang. The binary links to the system OpenSSL (OS security patches).
One command for XAMPP. .\install.ps1 → lk-cgi.exe + httpd.conf patch. Unlimited connections via IOCP.
A full hosting panel for Plesk 18+ — code editor, live monitor, log viewer. Add a domain, write your code, go live.
⇓ look-lang-plesk-1.0.0.zip → GitHub →IntelliSense (all built-ins + your own code), live error checking (syntax + undefined functions), outline, 20+ snippets, Run/Serve/REPL shortcuts. v2.0
⇓ Install from Marketplace → ⇓ look-lang-2.0.0.vsix (GitHub) →C++23, CMake 3.20+. Apache 2.0 license. For contributing or building it yourself.
GitHub → codlook/look All Releases