wawesome
v0.8.0
Published
CLI tool for building and deploying serverless functions on wawesome.io platform
Readme
🚀 wawesome
Official CLI for building, bundling, and deploying serverless WebAssembly functions on the wawesome.io platform.
Deploy ultra-fast, lightweight serverless WebAssembly functions directly from your terminal in under 60 seconds.
⚡ Quick Start
1. Create an account
Sign up for free at https://wawesome.io to set up your workspace.
2. Authenticate CLI
Login via browser OAuth:
npx wawesome login3. Initialize a project
Create a new directory and scaffold a WebAssembly TypeScript starter function:
mkdir my-wasm-app && cd my-wasm-app
npx wawesome initinit asks for the function name and for the App slug — the App groups the Functions of one
project, and its slug is part of the public URL your client sees. Both default to the directory
name, so naming is usually a matter of pressing enter.
An App slug must be a legal hostname label: lowercase letters, numbers, and single hyphens between
them (no leading or trailing hyphen, 63 characters at most). Type something else — My Client —
and the CLI shows you the slug it would become (my-client) and asks again, rather than rewriting
your answer behind your back.
Starting from a template
npx wawesome templates lists what is available; --template starts from one:
mkdir acme-store && cd acme-store
npx wawesome init --template stripe-webhookThis goes from nothing to a deployed Function in one command. The template is downloaded over
plain HTTP and unpacked — git is not required, on any platform — and its files land exactly as
they are published. Only the name in package.json, the App and Function names in
wawesome-function.json, and the wawesome dependency are touched — the last so the project you
get depends on the CLI that scaffolded it rather than the one the template was released against.
Templates declare what they need rather than shipping placeholders, so the CLI then asks for each
environment variable the template requires — showing where in the provider's own dashboard to find
the value — stores them (secrets write-only), enables any outbound providers the template calls,
and deploys. If a value does not exist yet, leave it blank; the CLI tells you the env set command
to run once it does.
Dependencies are installed for you (with whatever package manager launched the CLI — npx, pnpm
dlx, bun x), so the types are there when you open the project and the bundler can resolve the
template's imports. Pass --no-install to do it yourself. A failed install never stops the flow;
the command to re-run is printed.
Before any of that is sent anywhere, the CLI shows you the public URL your Function will have
and offers to change your workspace address — the part of that URL that is yours rather than this
project's, and the same one wawesome workspace show prints. It is randomly minted at signup and
stays changeable until your first deploy, at which point it locks for good, because live URLs carry
it. Press enter to keep it. If it is already locked, the offer is not made and the reason is said
plainly.
Being logged in is checked before the first question, not at the deploy: an expired session offers you a login there and then, and declining still leaves you the project plus the two commands that finish it.
Run it in an empty directory: if any file would be overwritten, nothing is written at all and the collision is named.
4. Build & Deploy
Deploy your serverless function to Wawesome Cloud instantly:
npx wawesome deploy📖 Command Reference
| Command | Description |
|:----------------------------------------|:--------------------------------------------------------------------|
| npx wawesome login | Authenticate CLI with your Wawesome account via browser |
| npx wawesome logout | Log out and clear saved credentials from your machine |
| npx wawesome whoami | View current logged-in user, workspace, and gateway info |
| npx wawesome templates | Browse the template catalog (no login required) |
| npx wawesome init | Scaffold a new serverless function project in the current directory |
| npx wawesome init --template <name> | Scaffold from a catalog template, wire up what it needs, and deploy |
| npx wawesome build | Bundle TypeScript entry code into an optimized JS bundle |
| npx wawesome deploy | Build, upload, and promote a function version to production |
| npx wawesome logs [func] | List recent past invocations for a function |
| npx wawesome logs --invocation <id> | Fetch full stdout/stderr log body for a specific invocation |
| npx wawesome logs <func> --follow | Follow live output (waits for the next invocation if needed) |
| npx wawesome invoke [func] | Fire a function run immediately and follow its output live |
| npx wawesome cron [func] | List schedules for a function or app |
| npx wawesome cron pause <name> | Pause a schedule by name (survives future deploys) |
| npx wawesome cron resume <name> | Resume a paused schedule (no backfill) |
| npx wawesome cron history [func] | Read background run history for scheduled and manual runs |
| npx wawesome version list | List version history for the current function |
| npx wawesome version switch <v> | Roll back or promote a specific function version |
| npx wawesome env list | View environment variables for the current app |
| npx wawesome env set <key> <val> | Set an environment variable (add --secret for write-only) |
| npx wawesome env rm <key> | Delete an environment variable |
🧭 Unsupported Globals
Every build scans the bundle it just produced against the platform's declared guest surface, and says nothing unless it finds something.
Intlis not provided. Where the bundle reaches it as it loads — your own module scope, or a dependency's — the build is refused before a deploy uploads anything: that bundle would not evaluate on the platform. Where the reference sits inside a function that may never be called, behind atypeofcheck, or inside atry, you get a warning and the deploy proceeds.toLocaleString,toLocaleDateString,toLocaleTimeString,toLocaleLowerCaseandtoLocaleUpperCaseignore their locale argument. They run and return an unlocalised answer, so these warn.
Each message names the global, the file and line in your source, and what to do about it. Declaring
an Intl polyfill in your package.json — the same declaration local parity
reads — silences the report, as does installing one
on globalThis in the bundle itself. The scan reads static references only: a global reached through
globalThis['Intl'] is invisible to it, so it never refuses a deploy on a guess. deploy
--skip-build scans the bundle it found on disk before uploading it.
📜 Invocation Logs
Inspect past function runs or view raw stdout / stderr log outputs directly in your terminal.
1. List Recent Invocations
List past executions (including status, trigger type, timestamp, and duration) for the function in the current directory:
npx wawesome logsOr list invocations for a specific function by name:
npx wawesome logs my-functionFilter by invocation status — only show errors, timeouts, etc.:
npx wawesome logs my-function --error
npx wawesome logs my-function --status timeout
npx wawesome logs my-function --running2. View Invocation Log Body (stdout/stderr)
Fetch and print the captured console.log / console.error text for a specific invocation:
npx wawesome logs --invocation 019fb344-ea0c-78f2-8a9b-d04e188b9823Or pass the UUID directly as the target:
npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b98233. Follow Live Output (--follow)
Stream an invocation's output as it runs — like tail -f for your serverless function.
Follow by function name (recommended)
npx wawesome logs my-function --followIf the function is currently running, its output is streamed immediately. If the latest invocation
already finished, the CLI waits for the next invocation to start and then streams it live.
Press Ctrl-C at any time to stop.
Follow a specific invocation by ID
npx wawesome logs 019fb344-ea0c-78f2-8a9b-d04e188b9823 --followReconnection
On transient network errors or server issues (5xx), the CLI automatically reconnects with exponential back-off (up to 3 retries). Non-recoverable errors like authentication failures (401) or unknown invocations (404) exit immediately with a clear message.
⚡ Manual Invocation
Fire a background run of a Function immediately without waiting for a schedule tick or deploying code:
# Invoke the function in the current directory and follow its output
npx wawesome invoke
# Invoke a specific function by name
npx wawesome invoke my-function
# Send custom HTTP method and request body
npx wawesome invoke -m POST -d '{"event":"audit"}'
# Fire without following live output
npx wawesome invoke --no-follow⏰ Schedules & Cron Management
Manage recurring Schedules and inspect background run history directly from your terminal.
1. List Schedules
List a Function's Schedules with expression, state, and next run in UTC:
# List schedules for the function in the current directory
npx wawesome cron
# List schedules for a specific function
npx wawesome cron list my-function
# List schedules across all functions in an App
npx wawesome cron list --app my-appThe output clearly distinguishes the three off-states:
paused: stopped by a user, resumable withwawesome cron resume <name>not in this config file: disabled because it was removed from configuration, resumable only by declaring it again in codesuspended: suspended by the non-payment ladder, resumable only after settling workspace balance
2. Pause and Resume Schedules
# Pause a schedule by name (stops queued ticks and survives future deploys)
npx wawesome cron pause nightly-reconcile
# Pause with an optional reason for incident context
npx wawesome cron pause nightly-reconcile --reason "database maintenance"
# Resume a paused schedule (recomputes next run from now, no catch-up backfilling)
npx wawesome cron resume nightly-reconcile3. Read Run History
Inspect past scheduled and manual runs, showing when each run was due, when it started, pool delay, and how it ended:
# View run history for the current function
npx wawesome cron history
# Filter run history by state (pending, running, dispatched, skipped, missed, cancelled, lost, failed)
npx wawesome cron history my-function --state failed⚙️ Configuration & Custom Gateway
wawesome-function.json
Every project directory includes a wawesome-function.json file generated during npx wawesome init:
{
"app": "my-app",
"function": "hello-world",
"entry": "src/index.ts"
}app is the App this Function is deployed into, and it is client-facing — every deploy from this
directory is scoped to it.
function is the address. Changing it does not rename anything: your Function's URL is built from
its name, so the next deploy lands on a Function of its own and the old one stays live at the old
URL, serving the code its callers already hold. The CLI remembers where this directory last
deployed and asks before that happens, naming both URLs. If you meant it, delete the old Function
from the dashboard once nothing calls it.
Add "assets" to deploy static files beside your code:
{
"app": "my-app",
"function": "hello-world",
"entry": "src/index.ts",
"assets": "dist/client"
}Everything under that directory is deployed with the version and served at its path beneath your
Function's URL — dist/client/assets/index-a1.js answers at https://<app-host>/<function>/assets/index-a1.js.
The CLI hashes each file and asks the platform which of them it does not already hold, so a redeploy
that changed one chunk uploads one chunk — and a deploy that changed nothing at all is refused
before a byte moves.
Files are served straight from object storage; your Function is never invoked for one, and no
invocation is recorded. They answer on your App's own hostname and nowhere else — on the
development path form (/x/<tenant>/<app>/<function>/...) the same address reaches your handler
as it always has, because a file on an origin every workspace shares would be same-origin with
all of them. Each carries Cache-Control: public, max-age=31536000, immutable and an
ETag, so name your build output by content hash — a file's bytes must never change under a name a
browser has already cached for a year. The content type comes from the extension against a fixed
allowlist and is never sniffed; anything off it is served as a download.
Two rules to know about:
- Everything beneath
assets/is static, whatever the deploy carries. A request there never reaches your handler — an unknown path under it is a 404, not a route for you to answer. - At most 100 files may sit outside
assets/. Those paths travel on the version record so a request can be routed without a lookup per file. Put bulk output underassets/, where a file costs nothing;favicon.ico,robots.txtand a.well-known/directory are what the rest is for.
HTML is refused at deploy time. Your Function renders its own markup, and a document served from your App's own origin is the sharpest same-origin vector a static file has.
An SVG is served with script denied. The rule behind the refusal above is that nothing you deploy
as a file runs script on your App's own origin, and an SVG opened directly in a browser would. It is
served rather than refused because denying it costs the file nothing: an <img src="logo.svg"> never
ran that script, so your drawings render as they always did — every SVG and XML file carries
Content-Security-Policy: script-src 'none', and only navigating straight to one loses anything.
Schedules
Add "schedules" to run a Function on a recurring timer, with no caller:
{
"app": "my-app",
"function": "nightly-reconcile",
"entry": "src/index.ts",
"schedules": [{ "name": "overnight", "expression": "0 3 * * *" }]
}An expression is five fields, read in UTC — minute, hour, day of month, month, day of week. There is no seconds field, and no timezone: a local zone would make one night a year fire a job twice and another night not at all.
The name is yours to choose and is what the platform keys the schedule by, so editing an expression is a change to the same schedule rather than the deletion of one and the creation of another — its history and its paused state stay attached to it.
Your deploy applies them and prints each one with the time it will next run. It is refused, before anything is built or uploaded, if an expression cannot be read, if it would run more often than every five minutes, or if one Function declares more than five schedules.
Two rules worth knowing before you edit the file:
- A schedule you delete from the file is disabled, not deleted. Its history stays, and declaring it again is what turns it back on — so a typo costs you a deploy rather than a job's record.
- A deploy never resumes a schedule a person paused. When it runs is code; whether it is running is not, and an unrelated commit the next morning must not restart what you stopped at 3am.
Leaving schedules out of the file entirely says nothing about them and changes nothing. Writing
"schedules": [] says this Function declares none, which disables the ones it used to have.
Keeping a Function off the web
A job on a timer should not also be sitting at a guessable URL where a stranger can fire it. Add
"visibility" to make a Function unreachable from the internet:
{
"app": "my-app",
"function": "nightly-reconcile",
"entry": "src/index.ts",
"visibility": "private"
}A private Function has no public address at all in production — not a hidden one, not one behind a credential. A request for it against your App's own hostname is answered with the same 404 as a Function that was never deployed. This is how you deploy a job with side effects without leaving it where a stranger who guesses the slug can fire it.
Leave the line out and your Function is public, which is what every Function without it has always been.
On your own machine, the local development surface serves Functions regardless of visibility, and
honours an explicit x-wawesome-trigger header so you can exercise a scheduled run by hand:
# Exercise a private or scheduled function locally on the development path form:
curl -X POST http://localhost:3000/x/my-tenant-slug/default-app/nightly-reconcile \
-H "x-wawesome-trigger: schedule"Production strips the reserved header namespace inbound, so the same header against your App's own
hostname reaches nothing that reads it: the run is a caller's, and a private Function is a 404
either way. Skipping your own authorization for a schedule run therefore opens nothing.
Making a Function private takes nothing but the deploy. Making it public again does not: deleting the line is refused, and the deploy tells you so having written nothing.
npx wawesome deploy --publishThat is deliberate — the line that keeps a job off the internet is one line, and a deploy that quietly honoured its deletion would put the job back on the open internet with nothing said. Every deploy prints the visibility it landed, beside the URL or in place of it.
Reserved headers
x-wawesome-* belongs to the platform in both directions. It is stripped off the request before your
handler sees it, and off your response before the caller does — so do not name a header of your own
on that prefix: it is dropped silently rather than rejected, and you will not get an error telling
you why it vanished.
Four headers arrive or leave on it, and the stripping is what makes them worth trusting:
| Header | Direction | What it means |
| --- | --- | --- |
| x-wawesome-forwarded-prefix | inbound | The mount that was stripped from the path. Join it to the path you observe to rebuild the caller's URL. |
| x-wawesome-trigger | inbound | How this run started: caller when someone called your address, schedule when fired by a Schedule. A caller cannot forge it in production (stripped inbound). On the local development surface, pass x-wawesome-trigger: schedule to exercise a background run by hand with the collapsed budget. |
| x-wawesome-invocation-id | outbound | The id of this run — the key to fetch its logs with npx wawesome logs --invocation <id>. |
| x-wawesome-error | outbound | Present only when the platform failed, never when your Function did. Its absence means the status on the wire is yours — up to the moment your response is committed, and no further. |
Testing against the guest's JavaScript surface
Your Function does not run on Node. The engine has no Intl, and its toLocaleString ignores the
locale you pass — (1234.5).toLocaleString('en-US') comes back as "1234.5", not "1,234.50". On
Node both work, which is how a green suite ships a Function that throws in production, or renders
markup the browser then refuses to hydrate.
Point your test suite at the guest's surface instead:
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: { setupFiles: ["wawesome/vitest-setup"] },
});Templates scaffolded with wawesome init --template ship this already. With it in place Intl is
gone, MessageChannel is the platform's own implementation rather than Node's, and the
locale-sensitive methods throw with a message naming the remedy — they throw rather than return the
engine's unlocalised answer because the platform declares them unsupported, and a wrong string that
fails nowhere is the thing this is here to stop you shipping.
If you bundle an Intl polyfill, declare it in your package.json as you normally would — a
dependency that provides Intl is left in place rather than stripped out from under you.
Local Development / Gateway Overrides
If you are running a local gateway or self-hosted instance, you can configure your CLI Gateway URL using any of the following:
1. Custom Settings (~/.wawesome/settings.json)
Create ~/.wawesome/settings.json:
{
"gateway_url": "http://localhost:3000"
}2. Environment Variables
export WAWESOME_GATEWAY_URL="http://localhost:3000"3. CLI Flag
npx wawesome login --gateway http://localhost:3000🔒 Security & Secrets
Wawesome encrypts environment variables at rest using two-tier envelope encryption (AES-256-GCM with per-app data keys
and AAD context binding). Use --secret when setting sensitive keys:
npx wawesome env set STRIPE_SECRET_KEY sk_live_xxx --secret🌐 Resources & Support
- Platform Homepage: https://wawesome.io
- Documentation: https://docs.wawesome.io
