Your documents stay yours
Your pages are created and handed straight back to you. This page says what the service holds on to, and for how long.
Summary
Picture the shop on the corner that cuts keys. You hand yours over, the machine copies it in a minute, and you walk out with both of them in your pocket. Asking us for a document works the same way. You hand over a description of the page you want, the machine creates it, and the page comes back to you.
While the page is being created, what you sent sits in the machine's memory, the way a telephone number sits in your head just long enough to dial it. The finished page travels back to you, and that memory goes straight to the next document waiting.
Five things are written down, and this is the whole list. A line for the bill, which says the name of your account, the moment, how many pages came out, and a number standing for that one document. A line in our own log for each document that comes out, saying the short name your account goes under, how many pages came out and how long it took. Your own fonts, if you send some and your account is set up for it: the shapes of the letters are put in a folder of your own, so that you never have to send them a second time. A short warning when your description mentions something the work then leaves out, such as a picture nobody placed or a colour nothing was painted with. And a line when we turn a request away, or when our own machine breaks, so that somebody can be woken.
When we cannot use what you sent us, the answer says so and shows you the exact bit that stopped it — the wrong word, the line it was on, the name you gave a picture — so that you can put it right without guessing. That answer goes back to you, in the reply to your own request, and it stops there: not one of those bits is written down, and none of them is passed on to anybody. The line we write says only that a request was turned away, and under which of a fixed handful of headings.
What is written on your pages stays between you and them. The words, the figures, your customers' names, the photograph you sent: they arrive, they are created, and they leave with the file. The bill knows how many pages there were, and that is the whole of what it knows.
The finished file carries what you asked for, and only that. Give it a title and it has that title; give it a date and it has that date. Everything else inside it is your own doing, which is why asking twice for the same document gives you the same file down to the last letter. That is something you can check yourself in a minute, and we would rather you did.
Two things you can ask for change that, and only these two: a field that asks for the moment the document is made, and a lock you pose without stating its key, which is then drawn at random.
This website and the machine that creates are two separate things. Here you have an account, an address for your invoice and your API key. Over there you are a short name and a page count. And if you would rather the whole of it sat on your own premises, that machine is sold to be installed there, in which case your pages stay inside your own walls from beginning to end.
Both of them stand on machines in France: this website and the machine that creates. A service we run for a customer of ours runs on machines there too.
Technically
The path a request takes end to end, the process boundaries it crosses, and every writer of the filesystem it puts in motion.
The request path, end to end
One POST carries a multipart body. The part named json_data holds the document description; every other part is a binary the description addresses by that name — a PDF template, an image, an ICC profile, an XMP packet, the XML of an invoice. The body is bounded by a configured ceiling, 512 mebibytes as the service ships.
Each part is read into memory and the description is deserialised into the request tree the service walks; the raw text is dropped the moment parsing returns. The writing itself runs on a blocking worker, so the async runtime carries on serving while a long document is composed.
The engine hands back a byte vector, which becomes the response body as it stands: no temporary file, no staging area, no second copy. The request tree, the named parts and the finished bytes are owned by that task and freed when it returns — in Rust the release is structural, not scheduled.
The unit runs with a private temporary directory, which the machine discards when the service stops.
The two writers of the filesystem
Two code paths in the whole service create a file, and each earns its place.
- The per-client font store. A client entitled to draw with fonts of its own has the fonts its renders actually used written under a directory named for it, and loaded back into memory at startup. It is server state rather than a cache: the bytes arrive once and every later request only names them.
- The alert queue. A line is written when the service breaks on its own side, and it is trimmed once the site takes it. Batches leave every five seconds or at a hundred alerts, whichever comes first, and the queue is read back at startup, so a restart loses none. It holds no path, no file name and nothing your request chose — an identifier, the sort of failure, the client name, an incident number and a moment.
What the log holds
Four statements sit on the render path. A document that came out writes the client name this site sent with the call, the number this site gave the call, the page count and the two timings. A warning from the engine writes the client name and the sort of warning; the render succeeds either way — a warning is an observation, never a refusal. A failure of the service's own writes an incident number, the client name and what broke. A refusal writes the client name, the status and the sort.
A warning names a resource the request supplied and the page never painted, by the label the engine assigned it while writing the file: a serial name for a font, another for an image, another for a colour space. Your own strings stay inside your document.
The remaining statements are lifecycle and alerting: the socket the service binds at startup, the stop it was asked for, the state of the trust store the alert feed verifies against, and what this site answered to a batch of alerts. Output goes to standard output, at the level the machine's journal collects.
What a refusal carries back
Every error the service raises is serialised into the body of the answer to the call that raised it, and a good many of them quote the request back. That is deliberate: a customer has to be able to see which piece of what they sent was refused.
The families that cite what you sent: the whole text of a code that could not be drawn, for each of the five kinds of barcode and square code; the value the description could not be read at, with its line and its column in the body you posted; the offending character, the text of a number and a variable name in a coordinate expression; the names you gave a font, an image, an attachment, a template, a layer, a colour space, a gradient, a drawing, a field, and an article identifier. Through the engine underneath: one character of the text being drawn, an article's title, and a field name with its answer.
All of it goes to the customer who sent it, in the answer to that call, and stops there. None of it is logged, none of it reaches the disk, and none of it is reported onwards: the twenty statements that write anything at all were read one by one, and not one of them carries an error message.
One thing of yours travels further: the client name this site sends with your call is written to the service's log on every call, and into the alert queue — and from there to this site — when the service breaks on its own side. Nothing else of yours travels: that failure answers you with a sentence carrying no filesystem path, and the path stays in the operator's log.
Authentication, and what the service is told
Authentication belongs to this site. Your call arrives here, the API key is matched against the accounts this site holds, and the call is forwarded to the service with one header holding the client name. The service has no authentication of its own: it answers on the loopback of this machine and nothing outside it can reach it, that header is the whole of what it is told about the customer, and it resolves entitlements — own fonts, page ceiling — from a TOML file read once at startup.
Revoking an API key is therefore a rewrite of that list, effective on the next request. The key itself never reaches the service.
This site's own access log, a separate question
The front server before these pages keeps an access log, as every front server does: source address, the account name when the request carried one, timestamp, request line, status code and body size, referrer, user agent, and the two names of the host asked for. It rotates daily and fourteen compressed days are kept.
It is read for one purpose, acquisition: which crawlers pass and at what cadence, whether an agent claiming to be one resolves as one, which published URLs have never been fetched, and which visitors arrived from a search. Static assets are served straight and are not among its lines.
An address that has repeatedly asked for URLs this site has never served is written into a short deny list the front server reads, and its later requests are answered at the door. That list holds addresses and nothing else, and it is edited by hand.
On your own infrastructure
Alert reporting is enabled by naming an endpoint to report to. Without one, no reporter task starts and no queue file exists, which leaves the font store as the single writer of the filesystem — and that one only for a client entitled to fonts of its own.
The mechanism to the line
The guarantees with their figures. Every line of it was read off the sources of the service, and holds for the version in production.
The request, step by step
- One endpoint creates a PDF, over a multipart body. The part named json_data carries the description as text; every other part is held under its own name and is the binary the description addresses by that name.
- Each part is read whole into memory, never streamed to a scratch file. The description is deserialised into the request tree, and the raw text is dropped as soon as that returns.
- The request tree and the named parts are moved into a blocking worker, and both are dropped when it returns. Nothing outlives the task: no cache keyed by request, no memoised layout, no shared buffer holding the last document.
- The body ceiling is a setting of the service, 512 mebibytes as it ships. A page ceiling may be set per server and per client, and the tighter of the two applies.
The two files the service writes
Two code paths in the whole service create a file. Here is each of them, field by field.
- The per-client font store: for a client entitled to draw with fonts of its own, the fonts a successful render actually set text with are written under a directory named for that client and read back at startup. It grows by exactly that — the fonts a document that came out really used. Each file is written to a temporary name in the same directory and renamed into place, so a concurrent read never observes a partial font; the name is held to ASCII letters, digits, a dot, a dash and an underscore, 64 characters at most.
- The alert queue: one JSON object per line, five fields and no others — an identifier the site deduplicates on, the sort of failure, the client name where one render is at fault, the incident number the customer was answered, and an RFC 3339 moment. It is written only when the service fails on its own side, never on a call it refused as faulty. A batch leaves every five seconds or at a hundred alerts, whichever comes first, and is trimmed from the file once this site has taken it; the queue is read back at startup, and this site deduplicates on the identifier, so a flush whose acknowledgement was lost is told once. The incident number is the only bridge to the service's log: the alert says there is something to look at, and the log line under that number says what it was.
The four statements on the render path
Four log statements stand on the render path, and this is all four. A document that comes out writes your client name, the number this site gave the call, how many pages came out and how long it took. A warning from the engine writes your client name and what sort of warning it was. A failure on our own side writes an incident number, your client name, and what went wrong here. A refusal writes your client name, the status, what sort of refusal it was, and — on the three sorts you cannot lift by editing your request — the same number you were answered.
That number is on your answer for one reason: so that a telephone call about a refusal reaches the log line it was written on rather than a log line that looks like it. It is carried by a missing licence, a document past its page ceiling and a failure of our own, and by nothing else — a description we could not read already names the JSON key to correct, and a number there would be an invitation to ring us about your own typing. The number counts nothing and names no machine of ours.
Neither a warning nor a refusal is named by its sentence: each is named by one word taken from a list fixed in the source, so nothing your request chose can reach the log through it. A word of that list reads like a resource left undrawn, a font whose licence forbids carrying it, a description we could not read — the sort of thing, never the thing.
Everything else is off the render path: the socket bound at startup, the stop the service was asked for, the state of the trust store the alert feed verifies against, and this site's answer to a batch of alerts. The service writes to standard output, at the level the machine's journal collects.
What travels back in an answer
A render that succeeded answers 200 with the PDF as the body, how many pages it has, how many things the engine noticed while creating it and how many findings stand between it and the standards the request asked for, each remark and each finding beside its count, and the number the call was made under when one was given. How long it took goes to the service's log and travels back in nothing.
A request the service could not use answers with a status code and a plain-text sentence naming what stopped it: 400 for a description it could not use, 402 for what an evaluation licence leaves to a licence key, 413 for a body or a page count past its ceiling, 500 for a failure on the service's side.
That sentence quotes the exact piece of your request that stopped it, so that you can put it right without guessing. Every error the service raises travels back that way, without exception, in the body of the answer to the call that raised it — and here is what a quoted piece can be.
- The text of a code that could not be drawn, whole, for each of the five kinds of barcode and square code. The answer names which of the five the request meant.
- The value the description could not be read at, with the line and the column it sits on in the body you posted.
- In an expression written where a coordinate was expected, the character that stopped the reading, the text of the number, and the name of the variable.
- The name you gave a font, an image, an attachment, a template, a layer, a colour space, a gradient, a drawing or a field, and the identifier of an article.
- Through the engine underneath: one character of the text being drawn where no font could draw it, the title of an article, and the name of a field with the answer standing in it.
Where a refusal stops
A refusal is the answer to your own call and its whole life ends there. One line is written when a call is turned away, and it holds three things: your client name, the status, and one word from a fixed list saying what sort of refusal it was. The sentence you were sent is not in it, and neither is anything your request chose: the twenty statements in the service that write anything at all were read one by one, and not one of them carries an error message. None of it is reported onwards either — the row this site bills from carries a client name, a moment, an identifier and a page count, and nothing else.
One thing of yours travels further, and it travels twice; that is the whole of it. Your client name is on every line the service writes about your call. It travels a second time when the service fails on its own side — a render that failed inside it, or a client font store it could not read or write: the alert it raises names the client the work was for, so that whoever is woken knows whose document it was, and it carries no path, no file name and nothing your request chose. Nothing else of yours travels at all: a failure answers you with a sentence carrying no path from our filesystem, and that path stays in the operator's log, where it belongs.
What the finished PDF carries
- The document information dictionary and the XMP packet are built from what your request states: title, author, subject, producer. A JSON key your request leaves out is one the file does not carry.
- A creation date appears when your request gives one: every date inside a finished document came from the request that asked for it. Where a request asks in so many words for the moment of the render to be put on the page, the host clock and the named time zone answer, and the date written is what those show.
- Every PDF has a file identifier. This one is a digest of the bytes just written, so the same description yields the same file twice over — which is what lets you diff two renders and see for yourself that they are identical to the byte.
- Under a licence key the pages come out clean. A copy running on an evaluation licence puts a watermark on every page it creates and says so on the page; it also draws the words as shapes rather than as letters, carries no font inside the file, writes no form and no structure, and refuses a handful of calls outright. The page on what a licence grants lists them one by one.
Your API key, and the name that stands for it
The API key lives on this site, and this site is the only place holding it. It is read here, once per call, and turned into a client name that travels on to the service in a header of its own, so the service never sees the key; it goes on the name.
On this site, one append-only row is kept per render: the account, the client name the render was made under, the identifier of that render, the moment, and the page count. A month of those rows is what an invoice is summed from, and the row names the client rather than the API key, so revoking one leaves the trail intact.
The website's access log, in full
The front server before these pages writes one line per request: the source address, the account name when the request carried one, the timestamp, the request line, the status code and the bytes sent, the referrer, the user agent, and the two names of the host asked for.
Lines rotate daily and fourteen compressed days are kept, which puts the oldest visit a fortnight back. Its lines are the pages a visitor asked for; the site's own static files and the requests turned away at the door are served straight.
A report in the site's own code parses those lines to answer five questions about acquisition: which crawlers passed and at what cadence, whether an agent claiming to be one resolves as one, which published URLs no crawler has fetched, which visitors arrived from a search and where they landed, and which requests looked for something this site has never served.
An address that has asked, several times over, for URLs this site has never served is written into a short deny list the front server reads, and its later requests are answered at the door. That list is short, it holds addresses and nothing else, and it is edited by hand.
Running the service yourself
Started without an endpoint to report to, the service keeps its own failures to its log and the two writers reduce to the font store alone. Started without a font directory for a client, that client's fonts live in the address space of the render that used them, and for exactly as long as it takes.
Where to go next
How a program asks for its pages The terms this is sold on Ask us anything this page left out