Static files and assets
Put files in
public/. There is no asset pipeline, no fingerprinting, and no file uploads.
public/
app.css -> /app.css
favicon.svg -> /favicon.svg
robots.txt -> /robots.txt
img/logo.png -> /img/logo.png
Anything under public/ is served straight from disk by melee-server, before any route is considered — for
GET and HEAD. A POST to a path under public/ still goes to your app, so a route can shadow a file for
one method and not the other. No route declaration, no helper, no manifest. The same files are served by
melee dev, which prints them differently so you can tell:
GET /app.css -> 200 (public/)
Reference them with plain paths:
<link rel="stylesheet" href="/app.css">
<img src="/img/logo.png" alt="">
What the server sends
HTTP/1.1 200 OK
content-type: text/css; charset=utf-8
cache-control: public, max-age=300
server: melee
content-length: 182
Content type from the extension, and a flat five-minute cache. That is deliberately conservative: there is no fingerprinting, so a longer cache would mean visitors stuck on an old stylesheet after a deploy. Better cache headers for content-addressed assets is a known open task.
If you want a long cache today, do the fingerprinting yourself — name the file app-7f3a.css, reference it
by that name, and change both when it changes. You still only get max-age=300, so this buys correctness
rather than speed.
There is no build step for assets
No Sprockets, no esbuild, no Tailwind CLI, no import. If you want compiled CSS or bundled JavaScript, run
the tool on your own machine and commit the output into public/ — melee push sends whatever is there.
Keep node_modules/ and source files out of the tarball with .meleeignore:
node_modules/
src/
*.scss
For most apps of this size, one hand-written stylesheet is genuinely the right answer.
There are no file uploads
This is the significant gap on this page. A melee app cannot usefully accept a file today:
request.formhandles urlencoded bodies only. Multipart is not parsed. A file input posts multipart, and the fields come back empty.- The whole request body is capped at 8 MB by the server.
- An app can only write to its own data directory, which is on the machine’s local disk and is not backed up for you.
- There is no object-storage connector and no request signing, so you cannot hand the bytes to S3 either.
If you need uploads, the honest options are to put them somewhere else (a form that posts directly to a service that accepts browser uploads) or to use a different platform for that app. An object storage connector with presigned URLs is designed but not built.
Serving a file from a route
You can always return bytes yourself, for something generated rather than stored:
# at the top of app.rb
require "csv"
get "/notes.csv" do
out = +""
out << CSV.generate_line(%w[id text done])
db.query("SELECT id, text, done FROM notes ORDER BY id").each do |row|
out << CSV.generate_line([row["id"], row["text"], row["done"]])
end
header "Content-Disposition", "attachment; filename=\"notes.csv\""
text(out, type: "text/csv; charset=utf-8")
end
Note +"" and <<: string literals are frozen in this dialect, so building a String starts with an unfrozen
one.
The response is collected in memory before it is sent — streaming is passed through as a whole today — so this is for kilobytes and megabytes, not gigabytes, and it runs under the 30-second request timeout.
See also
- Writing the response —
text,json,html,header,status. - What melee cannot do.