Hi, I'm Marc. For the last couple of years I've been building Faable, a platform where you deploy an app and, if it has users, get an OAuth 2.0 / OIDC server for them in the same account. It's hosted in the EU.
Instead of a feature list, here is what using it actually looks like: an Express app deployed from the terminal, then a login added to a frontend. Every command below is copied from our docs, and we timed the deploys on a fresh account this week.
1. Deploy an Express app from your terminal
No repository needed. Start with the smallest Express app there is:
mkdir hello-express && cd hello-express
npm init -y
npm install express
// index.js
const express = require('express')
const app = express()
const port = process.env.PORT
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, '0.0.0.0', () => {
console.log(`Listening on port ${port}`)
})
Add a start script to package.json:
"scripts": {
"start": "node index.js"
}
Then log in and deploy:
npx @faable/faable login
npx @faable/faable deploy launch --create hello-express
login uses a device flow, so it opens your browser and you approve it there. deploy launch --create makes the app and deploys the current folder to it. Three things happen:
-
Upload. The CLI snapshots the folder and uploads only the files the platform hasn't seen before, so the next deploy only sends what changed.
node_modulesand.envfiles stay on your machine. -
Remote build. The builder sees a
package.json, installs dependencies and runs your build step if you have one. Nothing builds locally: no Docker, no toolchain on your laptop. -
Release. The app goes live at
https://<app>.faable.linkwith TLS, and the CLI waits until the URL actually answers before it tells you it's done.
On a fresh account this took about 50 seconds for this app. A Flask app took about 90.
It runs as a normal long-lived process, not a serverless function: it starts once, keeps its state in memory while it's up, and a background loop keeps running between requests.
The bug that was in our own docs
Look at the app.listen line. Until this week, our Express guide had the arguments the other way round: app.listen('0.0.0.0', port, ...).
That version doesn't crash. It prints "Listening", and then never answers a request, because Node treats a non-numeric first argument as a path and opens a Unix socket file called 0.0.0.0 instead of a TCP port. It's the kind of bug that eats an afternoon, so if you have a container that "starts fine" and never responds, check the order: port first, then host.
2. Or deploy on every push
If your code is on GitHub or GitLab, connect the repo from the dashboard and every push to the default branch builds and goes live. The deploy status shows up on the commit. Same builder, same detection: Node.js (Next.js, Express, Fastify, NestJS, Astro, Vite), Python (Django, FastAPI, Flask) and PHP (Laravel or plain PHP).
3. Add a login
This is the part I care most about, because it's the part I kept rebuilding on every project.
In the dashboard you create a Client for your frontend and set its callback URL. Then, in a plain JavaScript single-page app:
npm install @faable/auth-js
// src/auth.ts
import { createClient } from '@faable/auth-js'
export const auth = createClient({
domain: 'your-domain.auth.faable.link',
clientId: 'YOUR_CLIENT_ID',
redirectUri: window.location.origin + '/callback'
})
// src/main.ts
import { auth } from './auth'
const app = document.querySelector<HTMLDivElement>('#app')!
async function render() {
if (location.pathname === '/callback') {
const { error, returnTo } = await auth.handleRedirectCallback()
if (error) return void (app.textContent = `Sign-in failed: ${error.message}`)
return void location.replace(returnTo ?? '/')
}
const { data } = await auth.getSession()
if (!data.session) {
app.innerHTML = `<button id="login">Sign in</button>`
document.querySelector('#login')!
.addEventListener('click', () => auth.signInWithOauthConnection({}))
return
}
app.innerHTML = `<p>Hello ${data.session.user.email}</p>`
}
auth.onAuthStateChange(() => render())
render()
That's the Authorization Code flow with PKCE. The sign-in button sends the user to a hosted login page with whatever you've switched on: Google, GitHub, passwords, and on paid plans email codes and MFA. Your app gets back a session with an access token you can send to your API as a Bearer token.
It's a standard OAuth 2.0 / OIDC server, so you don't have to use our SDK or host the app with us. Anything that speaks OIDC can use it, and the same server issues machine-to-machine tokens for service-to-service calls.
Things that might bite you
I'd rather you read these here than find them yourself:
- Free apps sleep after 30 minutes without traffic. The first request after that takes about 5 seconds while the app wakes up. On Hobby they sleep after 2 hours.
- New accounts can create 2 apps in their first 24 hours. It's an abuse limit. The error tells you when you can create the next one.
-
Your app has to listen on
$PORT. And, see above, in the right order.
Pricing
Free needs no card: apps sleep after 30 minutes, and you get 10 GB of egress a month. Hobby is β¬15/month: apps stay up for 2 hours without traffic, and Dockerfile builds are included. Pro is β¬99/month. Pricing is per project, never per user.
What I'd like to know
How long does it take you from sign-up to a live URL, and where do you get stuck? That first deploy is the thing I'm working on most right now, and I read every comment.
- Docs: https://faable.com/docs
- Express guide: https://faable.com/docs/deploy/guides/guide-express
- JavaScript login quickstart: https://faable.com/docs/auth/quickstart/javascript
Top comments (0)