AwaitIO

TypeScript · one Node process · your own app

Internal tools with just async/await

Write each tool as an async TypeScript function. Every await io asks a person in the browser and waits for the answer. The server and the UI run inside your own app, so there is nothing else to host.

How it works

Your code awaits, the browser answers

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

src/actions/users/createUserAction.ts
export const createUserAction = app.defineAction({ name: 'Create user',
handler: async ({ io, ctx }) => {
const email = await io.input.text('Email address');
const startsAt = await io.input.datetime('Start date');
const user = await createUser({ email, startsAt });
return { userId: user.id };
},
});
tools.example.com/actions/users/create_user

Create user

Start dateOct 20, 2026 9:00Time zone: Europe/Prague
Continue

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.

Company name
Acme Inc.
io.input.textDocs →

One line of text

Work email
[email protected]
io.input.emailDocs →

An email address

Webhook URL
https://acme.com/hooks
io.input.urlDocs →

A web address, returned as a URL

Agenda
io.input.textareaDocs →

Longer text

Credits
250▲▼
io.input.numberDocs →

A number with min, max and decimals

Send a confirmation email
✓The customer gets a copy of the change
io.input.booleanDocs →

Yes or no, with a checkbox

Starts at
Oct 20, 2026 9:00
io.input.datetimeDocs →

A date and time, as a luxon DateTime

Ships on
Oct 24, 2026
io.input.dateDocs →

A day, as a luxon DateTime

Plan
Team▾
Free
Team
io.select.singleDocs →

One option from a drop-down

Notify by
Add…▾
Email×
Slack×
io.select.multipleDocs →

Any number of options

DeviceStatus
Lobby screenOnline
Kiosk 2Online
WindowOnline
io.select.tableDocs →

Rows picked from a table

Delete 12 files?
ConfirmCancel
io.confirmDocs →

Ask before something irreversible

io.groupDocs →

Several inputs on one screen

NamePlanCredits
AdaTeam120
GraceTeam120
LinusTeam120
io.display.tableDocs →

Rows from your database, with links and paging

Users542
Admins3
PlanTeam
io.display.metadataDocs →

Label and value tiles

InvoiceOpen in Stripe ↗
io.display.linkDocs →

A link inside or outside the app

BillingPlan, invoices and refunds
io.display.headingDocs →

A section title

io.display.imageDocs →

A picture, such as a file preview

Refund policy
io.display.markdownDocs →

Formatted text in a step

{
id: 42,
plan: "Team",
owner: {…}
}
io.display.objectDocs →

A record as a JSON tree

Hi Ada, your order shipped.Track the package
io.display.htmlDocs →

HTML markup, sanitized first

+Bring your ownAny React component in one file, registered next to these and awaited the same way.io.input.yours()

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,
});

Free for small teams

Up to 3 users free, including commercial use. Larger teams pay one flat monthly price, and it is the same software either way.