Label Everything: A Thermal Printer as the Missing Home Organization Tool

A $30 thermal printer turned out to be the most impactful home organization purchase I've made. Here's how I use it for everything from storage bins to medicine bottles.

The Unlabeled Bin Problem

You know the scene. A wall of identical clear bins. Each one contains something. To find what you need, you open them one by one, rummage, close, repeat. Or worse — opaque bins where you can't even preview.

Labels solve this completely, but nobody labels things because the process is annoying. You need a label maker, the right tape, time to format the text, and enough motivation to do it for 40 bins in a row. The friction kills the habit.

So I connected a $30 thermal receipt printer to our home management system and made labeling automatic. Now when a storage location is created or updated in the inventory system, printing its label is one API call.

The Hardware

The printer is a standard ESC/POS thermal printer — the same type used for receipts at restaurants and shops. It uses heat-sensitive paper (no ink to replace), prints instantly, and the labels are surprisingly durable indoors.

It connects via USB and appears as a device file. The software sends raw ESC/POS commands — binary sequences assembled in a buffer. Here are the core commands:

// src/printer/printer.service.ts

const ESC = 0x1b;
const GS = 0x1d;

const CMD = {
  INIT: Buffer.from([ESC, 0x40]),
  BOLD_ON: Buffer.from([ESC, 0x45, 0x01]),
  BOLD_OFF: Buffer.from([ESC, 0x45, 0x00]),
  SIZE_XXS: Buffer.from([GS, 0x21, 0x00]),  // 1×1
  SIZE_XS: Buffer.from([GS, 0x21, 0x11]),   // 2×2
  SIZE_S: Buffer.from([GS, 0x21, 0x22]),    // 3×3
  SIZE_M: Buffer.from([GS, 0x21, 0x33]),    // 4×4
  SIZE_L: Buffer.from([GS, 0x21, 0x44]),    // 5×5
  SIZE_XL: Buffer.from([GS, 0x21, 0x55]),   // 6×6
  SIZE_XXL: Buffer.from([GS, 0x21, 0x66]),  // 7×7
  SIZE_XXXL: Buffer.from([GS, 0x21, 0x77]), // 8×8
  ALIGN_LEFT: Buffer.from([ESC, 0x61, 0x00]),
  ALIGN_CENTER: Buffer.from([ESC, 0x61, 0x01]),
  ALIGN_RIGHT: Buffer.from([ESC, 0x61, 0x02]),
  CUT_FULL: Buffer.from([GS, 0x56, 0x00]),
  CUT_PARTIAL: Buffer.from([GS, 0x56, 0x01]),
};

No driver installation, no print dialog, no fuss. The receipt paper comes in rolls that cost next to nothing. I've printed hundreds of labels from a single roll.

What We Print

The system supports several label types, each defined as a composable template. The template type system is what makes it flexible:

// src/printer/label-template.ts

interface TextSection {
  type: 'text';
  template: string;       // Handlebars template
  align?: TextAlignment;
  size?: TextSize;         // Uniform preset (XXS–XXXL)
  charWidth?: TextSize;    // Independent width multiplier
  charHeight?: TextSize;   // Independent height multiplier
  bold?: boolean;
  autoWrap?: boolean;
}

interface QrCodeSection {
  type: 'qrcode';
  data: string;            // Handlebars template
  moduleSize?: number;     // 1–16 dots per module
  errorCorrection?: 'L' | 'M' | 'Q' | 'H';
}

interface BarcodeSection {
  type: 'barcode';
  data: string;
  barcodeType: BarcodeType; // CODE128, CODE39, EAN13, etc.
  height?: number;
  hriPosition?: 'none' | 'above' | 'below' | 'both';
}

type LabelSection =
  | TextSection
  | DividerSection
  | BarcodeSection
  | QrCodeSection
  | ImageSection
  | SpacerSection
  | BannerSection;

interface LabelTemplate {
  name: string;
  sections: LabelSection[];
  preFeedLines?: number;
  postFeedLines?: number;
  cut?: 'full' | 'partial' | 'none';
}

Each template is just an array of sections with a cut style. Here's what we print with them:

Inventory Location Labels

These are the workhorse. Each label shows the location name in large bold text, the room, a description pulled from the container type, and the location ID. Stick one on each bin, shelf, or drawer. When you reorganize, reprint. The marginal cost of a label is effectively zero, so there's no penalty for changing your mind about what goes where.

Task Cards

Each task prints as a card with the task name, priority, due date, project, and a QR code linking back to the digital record. These go on the kitchen board for daily task management. Printed tasks are harder to ignore than notifications.

Medicine Labels

This one surprised me with how useful it became. The system generates multilingual medicine labels — English plus a translated language — using AI for the translations:

// src/printer/templates/medicine.ts

export const medicineTemplate: LabelTemplate = {
  name: 'medicine',
  sections: [
    {
      type: 'text',
      template: '{{name}}',
      align: TextAlignment.ALIGN_CENTER,
      size: TextSize.S,
      bold: true,
    },
    { type: 'divider', char: '=' },
    {
      type: 'text',
      template: '{{chemicalName}}',
      align: TextAlignment.ALIGN_CENTER,
      size: TextSize.XXS,
    },
    {
      type: 'text',
      template: 'Merek (ID): {{brandNamesIndonesia}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
    {
      type: 'text',
      template: 'Indikasi: {{indicationsId}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
    {
      type: 'text',
      template: 'Indications: {{indicationsEn}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
    {
      type: 'text',
      template: 'Dosis Dewasa: {{dosageAdult}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
    {
      type: 'text',
      template: 'Dosis Anak: {{dosageChild}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
    {
      type: 'text',
      template: 'Kontraindikasi: {{contraindicationsId}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
    {
      type: 'text',
      template: 'Peringatan: {{warningsId}}',
      size: TextSize.XXS,
      autoWrap: true,
    },
  ],
  postFeedLines: 3,
  cut: 'partial',
};

Living in Indonesia, having medicine information in both English and Bahasa Indonesia on the bottle means anyone in the household — including staff — can read and follow dosage instructions. We print these and tape them directly to the medicine containers.

General Purpose Labels

For anything that doesn't fit the templates above: food storage dates, kids' school supplies, cable identification, pantry organization. Customizable title, subtitle, and font size. Supports multilingual split layouts for our bilingual household.

Shipping Labels

Name, phone, address — straightforward but useful for the occasional package. No need to open a separate shipping app for domestic sends.

The Template Engine

Text sections use Handlebars templating, so the same template works across different data. The rendering pipeline assembles ESC/POS byte sequences from the template sections — setting alignment, size, bold, then encoding the text:

// src/printer/printer.service.ts — renderTextSection

private renderTextSection(
  section: TextSection,
  data: Record<string, unknown>,
): Buffer[] {
  const compiled = Handlebars.compile(section.template, { noEscape: true });
  let text = compiled(data).trim();
  if (false !== section.autoWrap) {
    text = wordWrap(text, resolveMaxChars(section));
  }

  const parts: Buffer[] = [];

  // Alignment
  const alignCmd = section.align === TextAlignment.ALIGN_RIGHT
    ? CMD.ALIGN_RIGHT
    : section.align === TextAlignment.ALIGN_CENTER
      ? CMD.ALIGN_CENTER
      : CMD.ALIGN_LEFT;
  parts.push(alignCmd);

  // Size: uniform preset or computed width×height
  let sizeCmd: Buffer | undefined;
  if (section.size && section.size in UNIFORM_SIZE_CMD) {
    sizeCmd = UNIFORM_SIZE_CMD[section.size as TextSize];
  } else {
    const w = resolveWidthMultiplier(section.charWidth);
    const h = resolveHeightMultiplier(section.charHeight);
    if (w !== 1 || h !== 1) {
      sizeCmd = Buffer.from([GS, 0x21, ((w - 1) << 4) | (h - 1)]);
    }
  }
  if (sizeCmd) parts.push(sizeCmd);

  if (section.bold) parts.push(CMD.BOLD_ON);

  parts.push(Buffer.from(
    applyCharSubstitutions(text, CHAR_SUBSTITUTIONS),
    'latin1',
  ));
  parts.push(Buffer.from('\n'));

  if (section.bold) parts.push(CMD.BOLD_OFF);
  if (sizeCmd) parts.push(CMD.SIZE_XXS);

  return parts;
}

The system handles automatic word-wrapping with English hyphenation — important when you're fitting a long item name onto a 58mm-wide label. It uses TeX hyphenation patterns to find legal break points:

// src/printer/printer.service.ts

function getBreakPoints(word: string): number[] {
  const hyphenated = hyphenateSync(word);
  const points: number[] = [];
  let pos = 0;
  for (const ch of hyphenated) {
    if (ch === '­') {  // soft hyphen
      points.push(pos);
    } else {
      pos++;
    }
  }
  return points;
}

The wrapping algorithm tries word boundaries first, falls back to hyphenation points, and only hard-breaks as a last resort. Eight text sizes are available, from XXS (48 characters per line, good for metadata) to XXXL (6 characters per line, good for bin labels you need to read from across the room).

Printing a label is just rendering the template with data and writing the buffer to the device:

// src/printer/printer.service.ts

async printLabel<T extends Record<string, unknown>>(
  template: LabelTemplate,
  data: T,
): Promise<void> {
  this.logger.log(
    `Printing label "${template.name}" → ${this.transport.target}`,
  );
  const payload = await this.renderTemplate(template, data);
  await this.transport.write(payload);
  this.logger.log(
    `Label "${template.name}" sent successfully (${payload.length} bytes)`,
  );
}

The Workflow That Sticks

The reason this works — and I've tried label makers before that gathered dust — is that the printing is embedded in the workflow, not separate from it.

When I create a new storage location in the inventory system, the label prints as part of that flow. When I categorize today's tasks, I print the batch. When we buy a new medicine, the label is generated alongside the inventory entry. There's no separate "labeling session" to schedule and inevitably skip.

The MCP integration means I can also trigger labels through conversation: "Print a label for Location 40" and the AI assistant fetches the location data from Notion, formats the label, and sends it to the printer. It's the kind of thing that feels like overkill until you're standing in the storage room with your hands full and your phone is the only thing you can reach.

What I'd Tell Someone Starting Out

If you're thinking about home organization and haven't considered a thermal printer, here's my advice:

  1. Get a 58mm or 80mm ESC/POS printer. They're commodity hardware. The cheap ones work fine.
  2. Label everything the day you organize it. If the label isn't on the bin when you put the bin on the shelf, it'll never get labeled.
  3. Make reprinting free. Thermal paper is cheap enough that you should never hesitate to reprint a label because you changed what goes in a bin.
  4. Connect it to your data. A label that pulls from your inventory system is always accurate. A hand-written label is accurate until you reorganize.

The printer cost $30. The paper costs a few dollars per roll. The time saved searching for things in unlabeled bins is immeasurable. Of all the components in our home organization system, the thermal printer has the best cost-to-impact ratio by far.