TypeScript

Backoffice written just like regular API routes

Each action is an async TypeScript function, like a route handler. Instead of reading a request, every await io asks a person in the browser and waits for the answer. There is no frontend to build and nothing else to host.

demo.awaitio.com/groups/users
The demo backoffice: the Users page with its actions, stats and a table of users

How it works

Your code awaits, the browser answers

Every await io.input call shows a form to the person running the action and pauses your function until they answer. Then it simply goes on.

src/actions/orders/refundOrdersAction.ts
01import { app } from '../app';
02import { db } from '../db';
03
04export const refundOrdersAction = app.defineAction({
05 name: 'Refund orders',
06 handler: async ({ io, ctx }) => {
07 const email = await io.input.email('Customer email');
08 const orders = await db.order.findMany({ where: { email } });
09
10 const toRefund = await io.select.table('Orders to refund', {
11 data: orders,
12 columns: ['number', 'total', 'placedAt'],
13 maxSelections: 10,
14 });
15
16 ctx.loading.start({
17 label: 'Refunding orders',
18 itemsInQueue: toRefund.length,
19 });
20 for (const order of toRefund) {
21 await db.refund.create({ data: { orderId: order.id } });
22 ctx.loading.completeOne();
23 }
24
25 return { refunded: toRefund.length };
26 },
27});
demo.awaitio.com/groups/orders
BackofficeDemo
CustomersOrdersBillingSupport

Orders

Every order placed in the stores.

Refund ordersResend receipt
NumberCustomerTotal
#48213Ana Ruiz$129.00
#48207Ana Ruiz$54.00
#48190Ana Ruiz$212.50
#48172Ana Ruiz$39.90

Components

Everything you can await

Each one is a call on io that shows a control and returns a typed value. Missing one? Write it.

What people build

Every tool your team keeps asking for

Anything a person should run, fill in or approve, written as one async function next to the code it needs.

Back offices

Look up and edit customers, orders and coupons without building an admin.

Customer support

Give support the few safe buttons they need, each one checked in code.

One-off scripts, made safe

Turn the migration you would run from a laptop into an action with inputs and a confirmation.

Reports and exports

Ask for a month or a customer and return a table, straight from your database.

Everything stays in your code and on your servers

Bring your codebase

A handler is a plain function in your own process. Your Prisma client, services, utilities and environment variables work as they are.

src/actions/orders/refundOrderAction.ts
import { app } from '../../app';
import { prisma } from '../../db';
import { formatMoney } from '../../utils/formatMoney';
export const refundOrderAction = app.defineAction({
handler: async ({ io, ctx }) => {
const orderId = await io.input.text('Order ID');
const order = await prisma.order.findUniqueOrThrow({ where: { id: orderId } });
const reason = await io.input.textarea(`Why refund ${formatMoney(order.total)}?`);
await prisma.refund.create({ data: { orderId, reason, approvedBy: ctx.user.email } });
return { refunded: formatMoney(order.total) };
},
});

Your own components

A component is plain React in one file, with its props and the value it returns. Register it next to the built-in ones and await it like any io method.

src/app.ts + src/actions/feedbackAction.ts
// app.ts
import { inputText } from '@awaitio/input-text';
import { AwaitIO } from 'awaitio';
import { rating } from './components/rating';
export const app = new AwaitIO({
name: 'Internal tools',
components: { input: { text: inputText, rating } },
});
// actions/feedbackAction.ts
export const feedbackAction = app.defineAction({
handler: async ({ io }) => {
const stars = await io.input.rating('How was it?', { max: 5 });
return { stars };
},
});

Environments with their own secrets

Production, staging and local are separate servers with their own secrets and data. AwaitIO deploys with the rest of your app, and the header switches between them.

src/app.ts
import { AwaitIO } from 'awaitio';
import { secrets } from './secrets';
export const app = new AwaitIO({
name: 'Internal tools',
secret: secrets.SESSION_SECRET, // a different value in each environment
environments: [
{ title: 'Production', color: '#dc2626', url: 'https://tools.example.com' },
{ title: 'Staging', color: '#f59e0b', url: 'https://staging.example.com' },
{ title: 'Local', color: '#16a34a', url: 'http://localhost:3000' },
],
users, components,
});

Private by default

Email and password login with scrypt hashes and signed cookies, runs visible only to the person who started them, and nothing sent anywhere: no hosted service, no telemetry.

src/app.ts
import { AwaitIO } from 'awaitio';
import { secrets } from './secrets';
export const app = new AwaitIO({
name: 'Internal tools',
secret: secrets.SESSION_SECRET, // signs the session cookies
users: [
{ email: 'alice@example.com', name: 'Alice', passwordHash: secrets.ALICE_HASH },
{ email: 'bob@example.com', name: 'Bob', passwordHash: secrets.BOB_HASH },
],
components,
});

AwaitIO is free for small teams, made possible by companies and people who sponsor it.