PDF Studio + Admin UI

Describe the PDF. Ship the C# class.

Design documents in plain English against a live preview, publish them to your App, and render them from strongly-typed C# - no binary templates, no Windows-only report designer, no AI at runtime.

PDF Studio: file explorer, typst template, and the live PDF preview

Built on typst

A format the models already know

typst is a modern typesetting language with a fast Rust compiler and enough public source for LLMs to write it fluently. That combination is what makes "add a payment status badge and highlight overdue rows in red" a workable instruction instead of a wish.

Documents as source code

Plain text you can diff, review in a PR and track in git - not a binary Word file someone edits by hand.

One fast binary

A Rust compiler renders the true PDF in milliseconds. No headless browser that lays out differently on every machine.

AI writes it well

Models handle typst fluently, so most documents are a few plain-English prompts rather than a layout project.

Pixel-identical, every time

The same template renders one customer or ten thousand, and each PDF comes out the same as the preview.

PDF Studio

Create and edit with text prompts

Template on the left, the real compiled PDF on the right, re-rendering as you type.

  • Rebuild what you already send - attach a screenshot or an existing PDF and get a template plus its data model back
  • Or start from nothing - describe the document and let the model draft it
  • Errors heal themselves - compile diagnostics are fed back for up to 3 repair passes before you ever see them
  • Undo is a button - revert cleanly to the pre-prompt state when a change doesn't land
The Edit with AI panel before a prompt
The same template after the prompt was applied
The formatting toolbar above a template
Attaching a reference image while editing an existing template
Attaching a screenshot to build a new template from an existing design
Page setup for size, orientation and margins
Choosing fonts and typography

Data-driven

The layout is the template. The content is JSON.

Every template has a .json sidecar holding its data, and a .ui.json JSON Schema describing the shape of it. That separation is what lets your App render the same design with its own data - one customer or ten thousand.

  • Edit as raw JSON, or through a form generated from the schema
  • Labelled fields, date pickers, enum dropdowns, add/remove rows for line items
  • No schema yet? Generate one from the sample data with a click
  • Either way the preview re-renders as you edit
A template's data edited through a schema-driven form
The same data edited as raw JSON

One shared library, every document

Brand colours, typography, headers, footers and helpers like #money() live in a shared lib.typ every template imports. Restyle every document your App produces by editing one file - and lib.preview.typ renders every component it defines on one page, so you can see the whole design system update as you change it.

lib.preview.typ rendering every component the shared library styles

Typed, not stringly

JSON Schema in, C# classes out

The generator turns a template's schema into real classes - so populating a document is new Invoice { … } with compile-time checking, not a dictionary of strings and a hope that the keys match.

  • A class per type, with [JsonPropertyName], List<T> and non-nullable members
  • Schema types carried through - multipleOf: 0.01 becomes decimal, format: date becomes DateTime, enums become real enums
  • Generated locally and instantly - no model call, no network
  • Python, TypeScript and JavaScript from the same schema

Generate the whole set into your project

Copying from the UI is fine for one model. For all of them, register an AppTask - the same arrangement as OrmLite Migrations, where writing into your source tree is something you ask for rather than something a running App does. Hand-edited files are skipped, not clobbered.

Configure.Pdf.cs
services.AddPlugin(new PdfFeature {
    PdfCodeGen = new() {
        Namespace  = "MyApp.ServiceModel.Pdf",
        OutputPath = "../MyApp.ServiceModel/Pdf",
    }
});

AppTasks.Register("pdf", _ =>
    appHost.GetPlugin<PdfFeature>().GeneratePdfs());
$ dotnet run --AppTasks=pdf
generated Invoice.cs
unchanged Receipt.cs
skipped   quote (Quote.cs was edited by hand)
C# classes generated from the invoice template's schema

/admin-ui/pdf

Published templates, live in your App

Publishing copies a template into App_Data/pdf where your App can render it. The Admin UI is where you browse what's published, run a template against real data with a live preview, and copy the code to use it.

Published PDF template gallery in the Admin UI
Browse published templates with thumbnail previews, search and sorting.
Live PDF workspace with JSON data and a rendered invoice preview
Test real data and inspect the compiled PDF side by side.
Schema-generated invoice form with nested fields and collection controls
Fill schema-generated forms, including nested objects and collections.
Invoice JSON data editor beside the live PDF preview
Edit the JSON payload directly and render the result.
Generated C# PDF data model with copy controls and rendering examples
Copy strongly typed models and ready-to-use rendering examples.

Real PDFs, not previews

Rendered with pdf.js from the actual compiled output, with zoom, fit, page count and download.

Exercise it with real data

Paste a payload or fill the form and watch it re-render - a faithful preview of what your App will produce.

Pick up someone else's

Edit opens it back in PDF Studio, copying the published files into your workspace first if a colleague published it.

Admin-only

The whole Admin UI requires the Admin role, like the rest of your App's admin surface.

In your App

Copy the code, ship the feature

The Code tab generates working examples against the template you're looking at - real type names, real members. Two of them cover most of what Apps need.

Return a PDF from an API

InvoiceServices.cs
public async Task<object> Get(DownloadInvoice request)
{
    var order = await Db.LoadSingleByIdAsync<Order>(request.Id);

    return await pdf.PdfResultAsync(MapToInvoice(order),
        $"Invoice-{order.InvoiceNo}.pdf");
}
Content-Type: application/pdf
Content-Disposition: attachment; filename="Invoice-INV-2026-042.pdf"

Email it from a background job

SendInvoiceEmailCommand.cs
[Worker("smtp")]
public class SendInvoiceEmailCommand(IPdfRenderer pdf, …)
    : AsyncCommand<SendInvoiceEmail>
{
    protected override async Task RunAsync(
        SendInvoiceEmail request, CancellationToken token)
    {
        var pdfBytes = await pdf.RenderPdfAsync(
            MapToInvoice(order), token);

        msg.Attachments.Add(new Attachment(
            new MemoryStream(pdfBytes),
            "invoice.pdf", MimeTypes.Pdf));

        await client.SendMailAsync(msg, token);
    }
}
Generated C# PDF API with a strongly typed invoice object initializer and live PDF preview
Start with a generated, strongly typed API and map your application data into the PDF model.

Production never needs a model

Authoring lives in ChatFeature; rendering is PdfFeature, and it stands alone. Design templates on a dev machine and deploy an App that has only the renderer - no API key, no provider, no designer, nothing to go down.

At runtime your App only ever touches App_Data/pdf and IPdfRenderer.

Render and save as you go

Download the compiled PDF at any point, or save a numbered snapshot - invoice-0001.pdf, invoice-0002.pdf - straight into the explorer tree, so you have a trail of what the document looked like as it evolved.

Saved PDF snapshots in the explorer
Downloading the compiled PDF

Put a real PDF in your App

Install typst, add the plugin, design a template, and render it from typed C#.