Skip to content

Getting Started

Ori Pekelman edited this page May 22, 2026 · 4 revisions

Getting started

This walkthrough goes from "you have a Linux/macOS shell" to "you have a single-binary HTTP server serving an authenticated CRUD endpoint." It assumes no prior tep knowledge.

1. Prereqs

You need:

  • A C compiler (cc, gcc, or clang).
  • GNU make.
  • CRuby >= 3.4 — used only by the bin/tep translator at build time. The compiled binaries have no Ruby dependency at runtime.
  • libsqlite3-dev if you want to use Tep::SQLite (most non-trivial apps do).

Linux:

sudo apt-get install build-essential libsqlite3-dev

macOS:

xcode-select --install            # cc + make
brew install sqlite ruby          # if you don't already have them

2. Install spinel

tep compiles via spinel, an ahead-of-time Ruby-to-C compiler:

git clone https://github.com/matz/spinel
cd spinel && make all
export PATH="$PWD:$PATH"     # so `spinel` is on PATH

make all produces three binaries: spinel_parse, spinel_analyze, spinel_codegen, plus a spinel shell wrapper that runs all three.

3. Build tep

git clone https://github.com/OriPekelman/tep
cd tep && make

make builds the C runtime helper (lib/tep/sphttp.c) and the bundled demo binaries. It also runs make test if you add the test target. The first build takes ~15 seconds on a modern laptop.

4. Your first app

Create hello.rb:

require 'sinatra'

get '/' do
  "hello from tep"
end

get '/hi/:name' do
  "hi, " + params[:name] + "!"
end

Build it:

tep build hello.rb       # writes ./hello (an executable)

Serve it:

./hello -p 4567          # default 1 worker; --workers N for prefork

In another shell:

curl localhost:4567/
curl localhost:4567/hi/world

The binary is around 80 KB. It links against libc and (if you used Tep::SQLite anywhere) libsqlite3 — that's it.

5. Persistence

Add a counter that survives restarts:

require 'sinatra'

DB = "/tmp/hello_counter.sqlite"

on_start do
  db = Tep::SQLite.new
  if db.open(DB)
    db.exec("CREATE TABLE IF NOT EXISTS hits (n INTEGER)")
    db.exec("INSERT OR IGNORE INTO hits SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM hits)")
    db.close
  end
end

get '/' do
  db = Tep::SQLite.new
  if !db.open(DB)
    halt 500, "db open failed"
  end
  db.exec("UPDATE hits SET n = n + 1")
  n = db.first_int("SELECT n FROM hits", "")
  db.close
  "hits: " + n.to_s
end

on_start runs once per worker before serving; it's the right place for schema setup. See Tep::SQLite for the full surface.

6. Authentication

Sessions are HMAC-SHA256-signed cookies — no server-side storage. Set the secret once at boot and use the session[...] hash:

require 'sinatra'

Tep.session_secret = ENV.fetch("TEP_SESSION_SECRET")

post '/login' do
  if params[:password] == ENV.fetch("APP_PASSWORD")
    session["user"] = params[:user]
    redirect "/"
  else
    halt 401, "nope"
  end
end

get '/' do
  who = session["user"]
  if who.length == 0
    redirect "/login"
  else
    "logged in as " + who
  end
end

For password hashing (rather than a literal == against an env var), use Tep::Password. For JWT-based session-less auth, see Tep::Jwt.

For the full principal+delegate identity surface (human + agent identities, provider chain, OAuth2-grant issuance), see Tep::Auth.

Form bodies. params[:name] picks up form fields from both application/x-www-form-urlencoded and multipart/form-data. File-upload parts (<input type="file">) are skipped in v1; a separate req.files surface lands later.

7. Cross-cutting filters

Add CORS to every route, plus a security-header tail:

require 'sinatra'

before do
  cors = Tep::Security::Cors.new
  cors.set_origin("*")
  cors.set_allowed_verbs("GET,POST")
  cors.before(request, response)
end

after do
  Tep::Security::Headers.new.after(request, response)
end

The full filter API lives in Tep::Security. For request logging or rate limiting, you write your own Tep::Filter subclass and chain it from a before block.

8. Production deploy

The binary is statically linked against everything but libc and (if used) libsqlite3, so deploys are usually scp ./your-app server:/srv/

  • a tiny systemd unit. There's no Ruby runtime to install on the server.

A minimal systemd unit:

# /etc/systemd/system/your-app.service
[Unit]
Description=Your tep app
After=network.target

[Service]
ExecStart=/srv/your-app -p 4567 --workers 4
Restart=on-failure
User=www-data

[Install]
WantedBy=multi-user.target

The --workers N flag preforks N worker processes; with SO_REUSEPORT on Linux 3.9+ the kernel load-balances new connections across them. See the README's perf section.

9. Where next

  • The Sinatra compatibility matrix lists every Sinatra idiom and how tep handles it (or doesn't).
  • The batteries reference covers each Tep::* module with full API and examples.
  • The examples/ directory has end-to-end apps you can read straight through: the four-battery agentic_chat (sub-second WS push, multi-user, agent-spawn), the larger chatbot against any OpenAI-compatible endpoint, plus hello, blog, chat, websocket_echo.
  • For the lower-level Tep::Handler API and a peek at what the translator generates, see Spinel-direct in the main README.

If something breaks, file an issue with a minimal reproduction — tep deliberately exists to find spinel's edges, so "your app doesn't build" is a useful data point.

Clone this wiki locally