A lightweight, self-hosted edge functions runtime built on Bun. It is written in pure Vanilla JS and currently ships with only one production dependency: jose for JWT verification.
Comes with a built-in dashboard for managing functions, viewing logs in real-time, handling API tokens, and tweaking every setting from a clean UI.
Warning
DropFunctions is still under active development. Expect rough edges, missing polish, and bugs. There are no official releases yet, so for now you should treat the repository as pre-release software.
Most serverless platforms are heavy. You need accounts, CLI tools, YAML files, deploy pipelines. Sometimes you just want to spin up a function and call it from your app. That's what DropFunctions is for.
- Native JS & TS — drop a
.jsor.tsfile into/functions. Bun handles loading and TypeScript transpilation instantly. Notsc, no Webpack, no build step. - The "Guillotine" isolation model — each function runs in its own Worker Thread with a hard timeout watchdog. If user code hangs or loops forever, the worker is terminated and the runtime plus dashboard stay alive.
- Ultra-lightweight — the runtime is intentionally small and built to be auditable. You can understand the core in an afternoon instead of reverse-engineering a framework.
- Mobile-first dashboard — manage functions, inspect real-time logs over SSE, edit code, and revoke tokens from a phone without needing a desktop control panel.
- Minimal production footprint — Bun runtime, Vanilla JS codebase, SQLite for local state, and only one production dependency:
jose. - Docker ready — production and development compose files are included.
# Install Bun if you haven't: https://bun.sh
curl -fsSL https://bun.sh/install | bash
# Clone and install
git clone https://github.com/henriquemafra/dropfunctions.git
cd dropfunctions
bun install
# Copy the env template and edit it
cp .env.example .env
# Start the server
bun run devThe runtime starts on port 3000 and the dashboard on port 4000.
bun run dev is the full development mode and restarts the app when you edit the runtime or dashboard code.
cp .env.example .env
# Edit .env — at minimum change DASHBOARD_PASSWORD and DASHBOARD_SECRET
# Production
docker compose up -d
# Development (with hot-reload and source mounting)
docker compose -f docker-compose.dev.yml upCreate a .js or .ts file in the functions/ directory. That's it — the filename becomes the route.
export default async function(req, ctx) {
const name = new URL(req.url).searchParams.get('name') || 'world'
return Response.json({ message: `Hello, ${name}!` })
}Call it:
curl http://localhost:3000/functions/hello?name=John
# → {"message":"Hello, John!"}The repo also includes example functions in both JavaScript and TypeScript:
functions/firebase-example.jsandfunctions/firebase-example.tsfunctions/supabase-example.jsandfunctions/supabase-example.ts
Your function receives two arguments:
| Argument | What it is |
|---|---|
req |
Standard Request object |
ctx.user |
Decoded JWT claims (when auth is enabled) |
ctx.env |
Environment variable accessor |
ctx.log |
Structured logger (ctx.log.info(...), .warn(...), .error(...)) |
ctx.requestId |
Unique ID for this request |
ctx.functionName |
Name of the current function |
Export a config object to customize behavior per function:
export default async function(req, ctx) {
// your code
}
export const config = {
auth: true, // require authentication (overrides global AUTH_REQUIRED)
timeout: 10000, // custom timeout in ms (overrides DEFAULT_TIMEOUT_MS)
rateLimit: {
requests: 50, // max requests per window
window: '5m', // time window (30s, 5m, 1h)
},
}Every request goes through this pipeline:
Request → CORS → Route Match → Logger → Auth Check → Rate Limit → Execute → Response
If any middleware rejects (bad token, rate exceeded, etc.), the chain stops and returns the appropriate error response. Functions that don't exist return 404.
DropFunctions supports three auth modes:
AUTH_PROVIDER=none
AUTH_REQUIRED=falseAll functions are public. Simple.
AUTH_PROVIDER=supabase
AUTH_REQUIRED=true
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_JWT_SECRET=your-jwt-secretClients send a Supabase JWT in the Authorization header. The runtime verifies it and populates ctx.user with the decoded claims.
AUTH_PROVIDER=firebase
AUTH_REQUIRED=true
FIREBASE_PROJECT_ID=your-project-idSame idea — Firebase ID tokens are verified against Google's public keys.
The dashboard lets you create API tokens (prefixed with of_). These work alongside any provider — the runtime checks for of_ tokens first, then falls back to the configured provider.
Tokens can be:
- Scoped to specific functions
- Set to expire (30 days, 90 days, 1 year, or never)
- Revoked at any time from the dashboard
curl -H "Authorization: Bearer of_abc123..." http://localhost:3000/functions/helloEnabled by default. Tracks requests per IP per function.
RATE_LIMIT_ENABLED=true
RATE_LIMIT_REQUESTS=100 # max requests
RATE_LIMIT_WINDOW=1m # per time window (30s, 5m, 1h)When a client exceeds the limit, they get a 429 Too Many Requests response. Individual functions can override these limits via their config export.
Full reference — all optional, sensible defaults are used when not set.
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Runtime server port |
FUNCTIONS_DIR |
./functions |
Where your function files live |
NODE_ENV |
production |
Environment mode |
DEFAULT_TIMEOUT_MS |
5000 |
Max execution time per function (ms) |
| Variable | Default | Description |
|---|---|---|
AUTH_PROVIDER |
none |
none, supabase, or firebase |
AUTH_REQUIRED |
false |
Require auth globally |
SUPABASE_URL |
— | Your Supabase project URL |
SUPABASE_JWT_SECRET |
— | Supabase JWT signing secret |
FIREBASE_PROJECT_ID |
— | Firebase project identifier |
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_ENABLED |
true |
Toggle rate limiting on/off |
RATE_LIMIT_REQUESTS |
100 |
Max requests per window |
RATE_LIMIT_WINDOW |
1m |
Window duration (30s, 5m, 1h) |
| Variable | Default | Description |
|---|---|---|
CORS_ORIGIN |
* |
Allowed origins (comma-separated or *) |
CORS_METHODS |
GET,POST,PUT,DELETE,OPTIONS |
Allowed HTTP methods |
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL |
info |
debug, info, warn, error |
LOG_FORMAT |
json |
json or pretty |
| Variable | Default | Description |
|---|---|---|
DASHBOARD_PORT |
4000 |
Dashboard UI port |
DASHBOARD_USERNAME |
admin |
Login username |
DASHBOARD_PASSWORD |
admin |
Login password |
DASHBOARD_SECRET |
change-me-in-production |
Session cookie signing key |
DASHBOARD_SESSION_TIMEOUT |
1440 |
Session duration in minutes (default 24h) |
The dashboard runs alongside the runtime and gives you a full management interface.
- Functions — list, create, edit (with syntax highlighting), delete, enable/disable functions
- Logs — real-time log viewer with SSE streaming, filter by level/period/search, export and clear
- Tokens — create and revoke API tokens with optional scoping and expiration
- Environment — view and edit all
.envvariables grouped by category - Metrics — request counts, latency percentiles (p50/p95/p99), per-function breakdown
- Settings — runtime configuration without touching files
- Docs — full configuration reference built into the UI
On first login, the dashboard shows a guided setup wizard that walks you through:
- Server configuration (port, functions directory, timeout)
- Authentication provider setup
- Rate limiting
- CORS settings
- Dashboard credentials
The wizard writes everything to .env so you don't have to edit files manually.
- Sessions are signed with HMAC-SHA256
- Passwords are never stored in the database
- The dashboard is completely separate from the runtime — different port, different auth system
- All API routes require a valid session (except login/logout)
dropfunctions/
├── src/
│ ├── index.js # Server entry point
│ ├── loader.js # Function file loading + hot reload
│ ├── router.js # Route matching (/functions/:name)
│ ├── runner.js # Execution with timeout
│ ├── auth/
│ │ ├── firebase.js # Firebase token verification
│ │ └── supabase.js # Supabase JWT verification
│ ├── middleware/
│ │ ├── auth.js # Auth middleware
│ │ ├── cors.js # CORS handling
│ │ ├── logger.js # Request logging
│ │ └── rateLimit.js # Rate limiter
│ └── utils/
│ ├── env.js # Env var helper
│ ├── response.js # Response utilities
│ └── timeout.js # Promise timeout wrapper
├── dashboard/
│ ├── server/ # Dashboard API (Bun.serve)
│ │ ├── index.js
│ │ ├── db.js # SQLite + SSE event bus
│ │ ├── middleware/
│ │ └── routes/
│ └── ui/ # SPA frontend (vanilla JS)
│ ├── index.html
│ ├── app.js
│ ├── style.css
│ └── pages/
├── functions/ # Your function files go here
├── data/ # SQLite database (auto-created)
├── Dockerfile
├── docker-compose.yml
├── docker-compose.dev.yml
├── .env.example
└── package.json
docker compose up -dThis builds the image, mounts ./functions and ./data as volumes, exposes ports 3000 and 4000, and includes a health check.
docker compose -f docker-compose.dev.yml upMounts the src/ directory too so you can edit runtime code with hot reload via bun --watch.
docker build -t dropfunctions .
docker run -p 3000:3000 -p 4000:4000 -v ./functions:/app/functions -v ./data:/app/data --env-file .env dropfunctions| Component | Technology |
|---|---|
| Runtime | Bun |
| Database | SQLite (via bun:sqlite) |
| JWT | jose |
| Dashboard | Vanilla JS SPA |
| Editor | Ace Editor (CDN) |
| Fonts | Inter, JetBrains Mono, Space Grotesk |
No frameworks. No build step. No transpilation. Just JavaScript.
MIT
