DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Our free-licence script prints a token on every run, and in a dry run that token is either real or imaginary

Notifio is sold as a one-time licence: pay once, get an email with an activation code, type the code into the app. There are no accounts, no passwords and no sign-in on the website at all, which I like, and which means there is also no admin panel. So when somebody needs a licence without paying for one, press, a beta tester, a friend of the project, the answer is a script:

Usage (from server/):
  pnpm grant <email> [flags]

Flags:
  --base-only   Grant the app only; leave auto-reply locked.
  --no-email    Write the licence but don't send anything. Prints the token.
  --dry-run     Show what would happen. No writes, no email.
  --site <url>  Base URL used for the download link (default https://notifio.app).
Enter fullscreen mode Exit fullscreen mode

Note the default: a granted licence includes the paid auto-reply upgrade unless you say otherwise. The comp is more generous than the product, because the alternative is granting someone the base licence and then having them discover a locked feature with a buy button on it.

Idempotent, because the recipient has already saved the token

 * Safe to re-run for the same address: licences are keyed on email, so a repeat
 * run tops up whatever is missing (reactivating, adding auto-reply) and reuses
 * the existing token rather than issuing a second one. That matters because the
 * token is what the recipient has already saved.
Enter fullscreen mode Exit fullscreen mode

The email column is notNull().unique(), so "one licence per email" is a database constraint rather than a convention, and the script leans on it: look up by email, and if a row is there, patch only what is missing.

if (existing) {
  token = existing.token;
  const patch = { updatedAt: new Date() };
  if (!existing.active) {
    patch.active = true;
    changes.push("reactivated the licence");
  }
  if (opts.autoReply && !existing.autoReply) {
    patch.autoReply = true;
    patch.autoReplyPurchasedAt = new Date();
    if (!existing.autoReplySessionId) {
      patch.autoReplySessionId = `free-grant_${createId()}`;
    }
    changes.push("unlocked auto-reply");
  }

  if (changes.length === 0) {
    console.log("• Nothing to change — already active" + (existing.autoReply ? " with auto-reply" : ""));
  }
Enter fullscreen mode Exit fullscreen mode

Generating a fresh token for an existing customer would be the kind of helpfulness that breaks something: their old code stops working, and they find out when the app asks them to activate again.

Two UNIQUE columns that Stripe is supposed to fill

export const licenses = pgTable("licenses", {
  id: text("id").primaryKey(),
  email: text("email").notNull().unique(),
  token: text("token").notNull().unique(),
  stripeSessionId: text("stripe_session_id").notNull().unique(),
  active: boolean("active").notNull().default(true),
  autoReply: boolean("auto_reply").notNull().default(false),
  /**
   * Checkout session that paid for the upgrade. Unique so a webhook retry
   * cannot be mistaken for a second purchase. Null until upgraded.
   */
  autoReplySessionId: text("auto_reply_session_id").unique(),
Enter fullscreen mode Exit fullscreen mode

stripe_session_id is NOT NULL and UNIQUE because every real row comes from a Checkout session and uniqueness is what makes the webhook safe to retry. A comped licence has no session, and the column still has to hold something no other row holds:

// NOT NULL and UNIQUE, but nothing was ever paid. A synthetic id keeps
// the constraint satisfied and makes comped licences obvious in the DB.
stripeSessionId: `free-grant_${createId()}`,
Enter fullscreen mode Exit fullscreen mode

A prefix plus 24 hex characters. The constraint stays honest, and where stripe_session_id like 'free-grant%' is now the query for "who did we comp", which is a thing I actually want to be able to ask.

The careful line in the whole script is three lines up from that, in the top-up path:

// Only stamp a session id if the column is free. It's UNIQUE, and a real
// Stripe id already there means they paid for the upgrade — don't clobber
// the record of that.
if (!existing.autoReplySessionId) {
  patch.autoReplySessionId = `free-grant_${createId()}`;
}
Enter fullscreen mode Exit fullscreen mode

Somebody who bought the upgrade and later gets comped something else must not have their payment overwritten by a synthetic id. The column is the only record that money changed hands for that feature.

It checks what this run needs, not what it can do

if (!process.env.DATABASE_URL) {
  die("DATABASE_URL is not set. Run via `pnpm grant` or `node --env-file=.env …`.");
}
if (opts.sendEmail && !opts.dryRun && !process.env.RESEND_API_KEY) {
  die("RESEND_API_KEY is not set. Run via `pnpm grant`, or pass --no-email.");
}
Enter fullscreen mode Exit fullscreen mode

The email key is only required by a run that will actually send one, so --dry-run and --no-email work on a machine that has no sending credentials at all. Three lines, and the reason I notice them is that the usual version of this check sits at the top, demands everything, and makes the safest flag the one you cannot use.

The write happens before the email, and stays

if (error) {
  // The licence is already granted at this point, so this is recoverable:
  // re-run with --no-email and pass the token on manually.
  console.error("✗ Email failed to send:", error);
  console.error(`  The licence IS granted. Token: ${token}`);
  process.exitCode = 1;
}
Enter fullscreen mode Exit fullscreen mode

No rollback. A granted licence is not harmed by an undelivered email, the token is right there to pass on by hand, and deleting the row would be the one action that could break a person who has already activated. Exit 1 so a script wrapping this one knows, and a message that says which half succeeded.

And then there is this line

console.log(`• Token: ${token}`);
Enter fullscreen mode Exit fullscreen mode

It runs on every path, before any of the email branches, including under --dry-run. Two branches above it assign token, and the dry run reaches both:

Run What token holds What the line prints
pnpm grant [email protected] --dry-run, existing licence existing.token, read from the database a live credential, from a command that promised to change nothing
pnpm grant [email protected] --dry-run, no licence yet generateToken(), never stored a credential that does not exist anywhere

Both of those are surprises, and they are opposite surprises from one console.log.

The first is the mild one, and it is arguably a feature: when somebody writes in having lost their activation code, this script is the lookup tool, and a dry run is the correct way to ask. It is still a credential printed into a terminal and a scrollback by a flag whose entire promise is that nothing happens.

The second is the one that could cost an afternoon. generateToken() is twelve characters of randomness in a deliberately unambiguous alphabet, formatted NTFIO-XXXX-XXXX-XXXX, and in a dry run it is thrown away a few lines later when the process exits. It looks exactly like a real one. Paste it into a message to the person you are comping and they will type it into the app, the app will ask /api/validate, and the server will tell them, correctly, that no such licence exists.

The repair is small. Either move the print inside the branches that actually wrote something, or label it, which I prefer because the lookup use is genuinely the one I reach for most:

console.log(opts.dryRun && !existing ? `• Token: ${token} (not created, dry run)` : `• Token: ${token}`);
Enter fullscreen mode Exit fullscreen mode

One folder, two defaults, both right

The script next to this one submits URLs to search engines, and I wrote about it this morning. Its origin defaults to NEXT_PUBLIC_APP_URL, which is localhost in development, and it refuses to run against a local origin with an error explaining why.

This one hardcodes production:

const DEFAULT_SITE = "https://notifio.app";
Enter fullscreen mode Exit fullscreen mode

Same folder, opposite choices, and I think both are right. The IndexNow default is for a command whose output goes to other people's servers, so it should follow the environment and complain loudly when the environment is wrong. The grant default is for a command whose output lands in a human being's inbox with a download link in it, and there is no development mode in which emailing somebody http://localhost:3001/download is the desired behaviour.

The mail it sends is its own template rather than the purchase one, for a reason worth copying:

 * Deliberately not the activation email: that one opens with "thanks for
 * purchasing", which is wrong — and confusing — for someone who was never
 * charged.
Enter fullscreen mode Exit fullscreen mode

And what arrives is a code, a link to the download page, and a list of what is unlocked. The same two steps anybody who paid goes through, minus the paying, plus one line of output that I am about to go and qualify.

Top comments (0)