pre-release · v1.0.0-beta.7

pdf engine · e-invoicing · pure typescript

PDFs and e-invoices in pure TypeScript.

Write components, get paper - and XRechnung and ZUGFeRD e-invoices that pass the official EN 16931 and PDF/A checks. The engine is our own - no headless browser, no Java - and draws PDFs in Node or right in the browser.

invoice.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
renders to
  • XRechnung
  • ZUGFeRD
  • EN 16931
  • PDF/A-3
  • real pagination
  • Vue · React · Angular · Nuxt
  • runs in the browser
  • no chromium · no jvm
  • MIT

one library · every framework

One engine. Every framework.

Vue, React, Angular and Nuxt each get a binding that feels native: components where the framework thinks in components, a service and signals where Angular does. Underneath is the same engine, so a document looks the same wherever you write it.

  • pure TypeScript
  • no headless browser
  • no Java, nothing native to install
  • Node and the browser

@jasy/e-invoice · XRechnung · ZUGFeRD · Factur-X

Germany: mandatory from 2027

E-invoices that pass the official checks.

One typed object in, a conformant e-invoice out: the PDF a person reads and the EN 16931 data a machine reads. Java has Mustangproject, PHP has horstoeko/zugferd, Python has factur-x - this is the TypeScript answer.

invoice.ts
import { renderZugferd, type Invoice } from "@jasy/e-invoice";

const invoice: Invoice = {
  number: "INV-2027-001",
  issueDate: "2027-01-15",
  currency: "EUR",
  dueDate: "2027-01-29",
  seller: {
    name: "Northwind GmbH",
    vatId: "DE265013614",
    address: { line1: "Hauptstrasse 1", city: "Berlin", postCode: "10115", country: "DE" },
    contact: { name: "A. Schmidt", email: "billing@northwind.de", phone: "+49 30 123456" },
  },
  buyer: {
    name: "Globex AG",
    address: { line1: "Marktplatz 5", city: "Munich", postCode: "80331", country: "DE" },
  },
  lines: [{ name: "Consulting", quantity: 8, unit: "HUR", netUnitPrice: 120,
            vat: { category: "S", ratePercent: 19 } }],
  payment: { iban: "DE89370400440532013000", terms: "Payable within 14 days." },
};

const { bytes, xml } = await renderZugferd(invoice, { locale: "en" });

$ jasy validate invoice.pdf

ZUGFeRD · EN 16931 (CII) - valid

  • EN 16931 business rules

    KoSIT

    0 errors · 0 warnings
  • PDF/A-3 structure

    Factur-X XMP, sRGB, embedded XML

    13 of 13
  • veraPDF

    PDF/A-3b profile

    0 failed rules
Check your own invoice
  • Totals by construction

    Line nets, totals and the VAT breakdown are computed from your data - nothing typed twice, nothing off by a cent.

  • ZUGFeRD, XRechnung, Factur-X

    PDF/A-3 with the EN 16931 XML inside, or the XML alone - CII or UBL, one switch.

  • Your data stays home

    No service, no upload, no Java. It runs in your Node process, next to your data.

why trust it

Every claim on this page links to its proof.

  • An engine of its own

    Layout, text shaping and the PDF writer are ours, in TypeScript. No Chromium, no Java, no pdf-lib underneath.

    How it works
  • Measured against react-pdf

    Faster, AES-256 encryption by default, accessible to PDF/UA - each measurement with the code to check it.

    See the comparison
  • Checked, not claimed

    E-invoices pass the KoSIT EN 16931 rules and veraPDF. Check any file yourself - ours or anyone's.

    Open the validator
  • Accessible PDFs

    One flag emits a tagged PDF/UA-1 document that passes veraPDF's machine checks.

    Accessibility
  • Byte-stable output

    The same document renders to the same bytes, every time - an archived invoice stays reproducible.

    Output options
  • Anywhere JavaScript runs

    Node or the browser, plain TypeScript, Vue, React, Angular or Nuxt - the same engine underneath.

    All guides

one engine · two runtimes · same code

Runs on your server. And in the browser.

Pure TypeScript, no Node-only crutches. The same component tree produces the same byte-exact PDF whether it runs on a server or entirely in the browser - your call, not the library's.

on your server

Node, Nitro, Lambda, Workers

Generate invoices and reports in your API or backend - dependency-light, no Chromium to boot, no JVM to feed.

in the browser

100% client-side

Render the whole PDF in the user's browser - no server round-trip, works offline, and nothing ever leaves the device.

$const bytes = await renderToBytes(doc) // same call, server or browser

@jasy/vue · PDFs as Vue components

new · on npm

Your PDF is a Vue component.

Author a document the way you build an app - components, props, slots and your reactive data - then render it to a real PDF, right in the browser. Real layout and pagination underneath, fully typed props, and a familiar feel if you have reached for @react-pdf/renderer on the React side.

Read the Vue guide pnpm add @jasy/vue vue
Invoice.vue
<Document>
  <Page :gap="12">
    <Text :size="30" bold>{{ invoice.title }}</Text>

    <Row
      v-for="line in invoice.lines"
      :key="line.id"
      :justify="'between'"
    >
      <Text>{{ line.name }}</Text>
      <Text bold>{{ line.total }} €</Text>
    </Row>
  </Page>
</Document>
const bytes = await renderToPdf(Invoice) // → a PDF, right in the browser
  • Components & props

    Author a document the way you build an app - reusable components, props and slots.

  • v-for, v-if, computed

    Your reactive data drives the page; loops, conditions and computed values just work.

  • Renders in the browser

    The isomorphic engine turns the component tree into a PDF client-side - no server required.

@jasy/nuxt · the Nuxt module

new · on npm

Drop it into Nuxt.

One install, zero config. Author PDFs as components and render them right in the browser, or stream them from a server route - your choice, the same components and helpers on both sides.

Read the Nuxt guide npx nuxi module add @jasy/nuxt
in the browser
<script setup>
const { open, download } = usePdf(Invoice)
</script>

<template>
  <button @click="open">View PDF</button>
</template>
on a server route
// server/api/invoice/[id].get.ts
export default definePdfHandler((e) =>
  Document([
    Page([
      Text(`Invoice #${getRouterParam(e, "id")}`),
    ]),
  ]),
)
  • Zero config

    Add it to modules. Components and the render helpers are auto-imported - no wiring, no imports.

  • Client or server

    Render in the browser with usePdf, or stream from a Nitro route with definePdfHandler. Same vocabulary.

  • Nitro caching, lean bundle

    Cache a route per path + query in one option. jimp stays server-side - the browser bundle stays small.

@jasy/react · PDFs as React components

new · on npm

Your PDF is a React component.

Author a document in JSX - components, props, hooks and context - then render it to a real PDF, right in the browser. Real layout and pagination underneath: tables that carry their header across pages, newspaper columns, headers and footers that keep their space.

Invoice.tsx
<Document>
  <Page gap={12}>
    <Text size={30} bold>{invoice.title}</Text>

    {invoice.lines.map((line) => (
      <Row key={line.id} justify="between">
        <Text>{line.name}</Text>
        <Text bold>{line.total} €</Text>
      </Row>
    ))}
  </Page>
</Document>
const bytes = await renderToPdf(<Invoice />) // → a PDF, right in the browser
  • Components, hooks & context

    Author a document the way you build an app - props, hooks, context and your own components.

  • Typed by the engine

    Every prop is the engine's own option type, so every option autocompletes the day it ships.

  • Renders in the browser

    The isomorphic engine turns the element tree into a PDF client-side - or on a server, the same call.

@jasy/angular · PDFs the Angular way

new · on npm

PDFs that follow your signals.

A document is a function of its data. inject(JasyPdf) turns it into a real PDF, and pdfResource() renders it again whenever your signals change - right in the browser, with real layout and pagination underneath.

Read the Angular guide ng add @jasy/angular
invoice.pdf.ts
export const invoicePdf: PdfDocumentFn<Invoice> = (invoice) =>
  Document([
    Page({ gap: 12 }, [
      Text(invoice.title, { size: 30, bold: true }),
      ...invoice.lines.map((line) =>
        Row({ justify: "between" }, [
          Text(line.name),
          Text(`${line.total} €`, { bold: true }),
        ]),
      ),
    ]),
  ]);

// in a component: renders again whenever the invoice changes
readonly preview = pdfResource(invoicePdf, () => this.invoice());
<iframe [src]="preview.latest()?.safeUrl"> // → a live preview, right in the browser
  • Built the way Angular builds

    provideJasy() in the app config, inject(JasyPdf) where you need a PDF. A document runs in an injection context, so inject() works inside it.

  • A preview that follows your signals

    pdfResource() builds on Angular's resource(): it renders again on every change and keeps the last PDF meanwhile, so the preview does not blink.

  • ng add, and it is set up

    One command installs and configures it. Tested on Angular 20, 21 and 22, zoneless and with zone.js, with and without SSR.

roadmap · MIT · built in the open

1.0 is built. Here's what's next.

Built in the open, MIT-licensed. Everything the 1.0 set out to do is done and shipping; this is what comes after it - in order, no dates promised.

  • Read and edit any PDF

    Open a file, change what is on the page, and merge, split or extract. Reading a form and filling it already ships.

  • Digital signatures

    Sign a document so a receiver can prove nobody touched it afterwards - the piece an e-invoice still misses.

  • The last few percent of layout

    A public measure() helper, a heading across all columns, and a border that stays open where a box is split across pages.

  • Templates you can start from

    An invoice, a report and a letter that work as they are, and come apart when you want your own.

  • More e-invoice profiles

    Peppol and further country formats, on top of the Factur-X, ZUGFeRD and XRechnung profiles already supported.

  • Our own bidi

    Hebrew and Arabic already order correctly. The UAX #9 library behind that seam has not been touched since 2023 and ships no types, so it is going to be ours.

Follow on GitHubSee the full boardIssues, ideas and PRs welcome.