Build Sejda Web
YESreplaces $7.5/mosaves $90/yrback to the verdict
A local PDF workbench whose hard problems are redaction that actually removes rather than covers, a document graph where no operation ever overwrites its input, and running an office converter on other people's files without it hanging or reaching the network.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the service and the PDF work.
Get it python.org or your package manager. open ↗
Verify
python3 --version prints 3.12 or higher - installfree
Why Object-level work, full file rewrites and dropping unreferenced objects all come from here, and it is the part that makes redaction verifiable.
Get it Install the library and confirm the underlying qpdf version it links against.
Verify
A round-trip rewrite of a test PDF drops a deliberately orphaned object - installfree
Why Office to PDF conversion is one of the operations people actually want, and it is also the one that hangs, so it needs its own containment.
Get it Install the suite and confirm the headless conversion command runs.
Verify
A document converts with a per-run profile directory and no network access - have readyfree
Why Every hazard this build warns about needs a fixture, and collecting them afterwards means the warnings were never tested.
Get it Create or collect one PDF of each kind and keep them in the repository.
Verify
The suite covers encrypted, signed, form-bearing and scanned documents - decidefree
Why Redaction is the operation with real consequences if it is wrong, and offering it means committing to the extraction check on every output rather than treating it as a nice extra.
Get it Decide, and if yes, write the verification requirement into the release gate before building it.
Verify
The release gate names both redaction checks
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- documents(id, sha256, filename, bytes, page_count, encrypted, has_signatures, has_forms, imported_at) · sha256 is the identity, not the filename - operations(id, input_document_id, output_document_id, kind, args_json, started_at, ended_at, exit_state, tool_version, warnings_json) - pages(document_id, index, width_pt, height_pt, rotation, has_text_layer) · probed once on import, so a page range can be validated before anything runs - redactions(id, document_id, page_index, rect_json, matched_text_hash, applied_at, verified_at) · verified_at is set only after the output has been re-extracted and checked, and never by the code that did the removal - retention(document_id, expires_at, deleted_at) · a workbench full of other people's documents needs an end date - There is no statement anywhere in this application that changes a document's bytes. An operation reads one row and writes another, and the graph of operations is the history of the file.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
STORAGE_DIR | required | ./data/documents | Content-addressed originals and outputs. The largest thing here. |
DATABASE_URL | required | sqlite:///./data/workbench.db | Documents, operations, redactions and retention. Outlives the files. |
CONVERT_TIMEOUT_SECONDS | required | 120 | After this the converter is killed and the run recorded as a timeout. |
CONVERT_PROFILE_ROOT | required | ./data/convert-profiles | A fresh subdirectory per run, removed afterwards. |
RETENTION_DAYS | required | 30 | Documents are deleted after this. Operation rows and hashes remain. |
MAX_UPLOAD_MB | required | 200 | Refused above this, with the limit named, rather than failing part way. |
REDACTION_VERIFY | required | strict | Strict means an unverified redaction is never offered. There is no other sensible value. |
The build, in order
Import, identity and a preview that warns
A document's hash, structure and hazards are known before any operation is offered.
Filenames lie; the bytes do not.
Files
app/import.pytests/test_dedupe.pyEncryption, signatures and forms all change what is safe to run.
Files
app/probe.pyRemoving somebody's restrictions is a decision, not a side effect.
Files
app/ops/decrypt.pytests/test_encrypted.py
done when · tick each as it passeswatch out- Many PDFs carry an owner password with permission flags and open with no prompt at all, so a naive open-and-save writes an unencrypted copy and silently strips the restrictions the sender set, which is a decision you have made on somebody else's behalf without telling them; detect encryption on import, record it on the document row, and require an explicit decrypt operation that states what is being removed and appears in the log.
The structural operations
Merge, split, reorder, rotate and extract produce new documents and never surprise you.
And show the resulting pages before anything runs.
Files
app/ranges.pytests/test_ranges.pyTest it by filling the merged form, not by reading the spec.
Files
app/ops/merge.pytests/test_merge_forms.pyThe input row is never touched, by any code path.
Files
app/ops/base.pytests/test_immutable.py
done when · tick each as it passeswatch out- Merging two documents that both contain a form field called 'Name' produces a file where filling one field fills both, because form fields are addressed by their qualified name across the whole document rather than per page, and nobody notices until a filled form comes back wrong; detect name collisions before merging, rename or refuse with the collision listed, and test it by filling the merged form rather than by reading the specification.
Redaction that actually removes
Text marked for redaction is absent from the output file, proved by extraction rather than by looking.
The rectangle is the last step, not the operation.
Files
app/ops/redact.pyAn incremental save keeps the version you were removing.
Files
app/ops/redact.pydocs/redaction.mdTwo different checks, because they miss different things.
Files
tests/test_redaction_verified.py
done when · tick each as it passeswatch out- Drawing a filled rectangle over the text and saving leaves every one of those characters in the content stream for anyone who selects the region or runs an extractor, and saving incrementally leaves the pre-redaction version of the objects in the file as well, so you have shipped the secret twice; remove the objects, write a fresh file rather than an incremental update, do not preserve unreferenced objects, and treat the extraction check as the only evidence the redaction worked.
Conversion and OCR, in a box
Converting an office document cannot hang the service, reach the network or run anything the file asked it to.
Shared profiles are how one bad file poisons the next run.
Files
app/convert.pytests/test_timeout.pyA document should not be able to make outbound requests.
Files
app/sandbox.pyRendering differences between versions are real and surprising.
Files
app/ops/base.py
done when · tick each as it passeswatch out- Running an office converter with its default profile against a file somebody sent you means it can follow external references, attempt macro execution depending on how it is configured, and sit forever on a document that wants a password while your queue backs up behind it; give it a fresh profile directory per run, cut its network access, disable macros explicitly, set a timeout, and kill and record rather than waiting for a process that has already decided to wait for a human.
Signing, retention and the honest limitproduct builder
The signature flow says what it is, the documents expire, and the log survives them.
The README is read by you, not by them.
Files
app/ui/sign.tsxAny structural change invalidates the signature already there.
Files
app/ops/base.pytests/test_signed_warning.pyThe hashes outlive the bytes and that is the point.
Files
app/retention.py
done when · tick each as it passeswatch out- Calling what this produces a signature invites somebody to rely on it for something that matters, when it is a picture on a page plus a log saying who typed what and when, with no identity verification behind it and none of the standing a qualified or regulated electronic signature has; put the limitation next to the button where the signer will see it rather than in a README only the builder reads, and refuse to run structural operations on an already signed document without an explicit warning that doing so invalidates the existing signature.
That is the whole plan for Sejda Web. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Qualified or regulated electronic signatures.
- Government identity verification.
- Full page-content editing.
- A hosted service with accounts.
- excellent PDF operations, hosted convenience, desktop app, and edge-case handling
- pixel-perfect proprietary PDF engine
- identity verification
- qualified trust services
- large template and integration ecosystem
- A batch mode that applies one recorded operation to a folder, which is where the arguments-in-the-log design pays off
- A comparison view between an input and its output, which catches more mistakes than any warning
- Attachment and embedded file extraction, since that is where documents hide things nobody looked for
Need the files? The project pack on the verdict page hands your agent the whole brief · more documents & pdfs.