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.
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? ↓
AlmaLinux 8 · MariaDB · measured on a direct port, 64 workers.
All tests were measured with ab on a live AlmaLinux 8 server
(see test.codlook.com for applications running on it). 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.
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 |
| WebSocket masking | Unmasked client frame → connection closed with 1002 (RFC 6455 §5.1) — cache-poisoning / proxy defense, no silent accept |
| JSON stack overflow | Deeply nested JSON body → recursion depth limit (JSON_MAX_DEPTH) — stack overflow prevented |
| Parser stack overflow | Deeply nested ((…)) / [[…]] in source → expression/statement depth guards (150) — segfault prevented |
| Response splitting | CR/LF in a response header / cookie / redirect → stripped at the setter and at serialization — no header injection |
| Protocol parser DoS | RESP2/HTTP/PG/SMTP: unguarded number conversion + unbounded allocation closed (a malformed server response can't crash a worker) |
| Arithmetic UB | 64-bit integers; signed-overflow, shift and unary-minus undefined behaviour eliminated (promote to float only past ±9.2×10¹⁸) — UBSan clean. BIGINT / IDs / phone numbers round-trip exactly |
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.
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. The default engine everywhere: ~41–51× faster than the tree-walk interpreter on the CLI, and up to 7.8× on compute-heavy web 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. 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).
Embedded 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.
Raise your own errors with throw — a plain string or a structured error::new($type, $message [, $code]). Catch with try/catch; inspect via error::is($e, $type), 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.
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 portable binary for glibc ≥ 2.28: AlmaLinux/Rocky/RHEL 8·9·10, Ubuntu 18.10→24.04, Debian 10+ — and future releases work automatically (built on old glibc + static OpenSSL). CentOS 7 / RHEL 7 (glibc 2.17, EOL) is not supported — migrate to AlmaLinux 8.
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 php /usr/local/psa/admin/plib/modules/look-lang/scripts/post-install.php
Plesk does not run the extension's install hook, so this installs the LOOK binary to /opt/look and grants sudo access. Without it, enabling a domain fails with sudo: a password is required. Safe to re-run; survives uninstall/install.
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).
The official image is published on Docker Hub. One command downloads and starts LOOK — no source, no compiler. Your app lives in ./app, mounted at /app.
docker run -p 7400:7400 -v "${PWD}/app:/app" codlook/look
Put your index.lk in ./app, then open http://localhost:7400/. First run pulls the image (~43 MB) automatically.
Prefer building from source? The multi-stage production image (AlmaLinux 8 base) compiles and packages LOOK:
docker build -f cpp/docker/Dockerfile.production -t look:latest .
Build once on AlmaLinux 8 (glibc 2.28 + static OpenSSL) — the same single binary runs on every distribution (Ubuntu/Debian/AlmaLinux/Rocky/RHEL 8·9·10 and newer). No per-distro builds:
docker build -t look-build -f cpp/Dockerfile.build cpp/ && docker run --rm -v "${PWD}/cpp:/look/cpp" -w /look/cpp look-build bash build-portable.sh
Output: cpp/build-portable/{lk, lk-fcgi, lk-cgi} — statically linked (glibc + libz only). Verified on Ubuntu 22.04 and AlmaLinux 9 (OpenSSL 3.x).
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