BitelioBitelio

npx bitelio init

Set up transactional emails from your repository in one command — what it reads, what it writes, and what it refuses to do

One command, run in the root of your application:

npx bitelio@latest init

@latest matters if your project already depends on the bitelio SDK. npx prefers a copy already installed over fetching one, and versions before 0.2.0 had no command in them — a plain npx bitelio init there fails with npm's could not determine executable to run, which explains nothing. Naming the version skips the local copy.

It signs you in, gives you a project and a key, reads your repository, proposes the transactional emails your application should send, and opens a pull request with the code to send them.

Two things to know before anything else.

The key it gives you is a test key. A test key does everything a live key does and stops before the provider: every send is recorded, none is delivered. That is what lets your first send work in the same sitting rather than waiting on DNS — and it is also the one way this can bite you. A test key in production configuration is an application that deploys, runs, reports success on every send, and delivers nothing. Mint a live key in Settings → API keys when you are ready to send for real.

New projects wait a few hours before they can send live mail. Test sends are exempt, so nothing here is blocked by it, but a live send from a project created minutes ago is refused. The CLI tells you the time it unlocks.

What it reads

Only files a rule can justify, and it shows you every one before anything leaves your machine:

It looks forTo learn
package.jsonyour framework, whether you use Stripe, and your test command
app/ or pages/which router, which decides where generated code goes
stripe.webhooks.constructEventyour Stripe handler — subscription, payment failure, cancellation
a Svix verificationyour signup handler — signup, email verification
schema.prisma or a Drizzle schemawhat your user model is called
the file tree, paths onlywhere everything else is, without reading it

Handlers are found by the call that makes them handlers, not by their path, so it does not matter where you file them.

No .env file is ever opened, whatever it is called — not even to inspect. Everything else passes a secret filter first: anything shaped like a key is replaced with <redacted>, and the screen tells you how many were replaced in each file. It is a heuristic and not a guarantee, which is why you get to read each file exactly as it will be sent, and drop any of them.

To see all of that and nothing else, with no account and no network:

npx bitelio@latest init --dry-run

What it writes

A branch of its own, from HEAD. If you have uncommitted changes it stops before doing anything, so whatever it produces you can throw away with one command:

git branch -D bitelio-init

It runs your test command before and after the patch. A check that was already failing stays failing, and the pull request says so rather than blaming the change. A check that passed and now fails means the patch is undone and you are put back exactly where you were. If your repository has no test command it runs nothing — and says so loudly in the pull request, because unverified and verified deserve different amounts of trust.

No key goes in the pull request. Only the variable name and a placeholder; the key itself was printed once in your terminal.

If the GitHub CLI is not installed or signed in, the branch stays and you get the compare URL.

When it stops

init builds your lifecycle from your Stripe integration — that is what tells it which events your application actually has. Without a stripe dependency it stops before uploading and says so, because guessing a lifecycle out of a repository that has none produces a pull request nobody can test.

The framework does not matter. Next.js, Express, Fastify, Remix, NestJS, Hono, or a plain Node service: the handler is found by the call that makes it one, stripe.webhooks.constructEvent, not by where you filed it. Naming your framework only helps it place NEW files idiomatically — with Next it knows app/ from pages/; elsewhere it follows how your project already organises itself, and the consent screen tells you when it is going on convention rather than certainty.

In a monorepo it names the workspace to run it from instead of guessing at one.

Running it unattended

The consent screen needs a person, so init does not upload from CI at all. --dry-run works anywhere. For a scripted run on a machine with a terminal, --project <id> picks the project and --yes takes the default where there is one; with several projects on the account and neither flag, it refuses rather than write into the wrong one.

Three runs per account per day.