Build Sejda Web

YESreplaces $7.5/mosaves $90/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimeA local FastAPI service with a browser interface, nothing hostedPDF engineA structural library over qpdf for object work, with a separate renderer for previewsStoreA content-addressed folder of originals plus SQLite for the operation logConversionAn office converter headless in a subprocess with a timeout, a fresh profile and no networkOutputEvery operation writes a new file and records input hash, arguments and output hash

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
STORAGE_DIRrequired./data/documentsContent-addressed originals and outputs. The largest thing here.
DATABASE_URLrequiredsqlite:///./data/workbench.dbDocuments, operations, redactions and retention. Outlives the files.
CONVERT_TIMEOUT_SECONDSrequired120After this the converter is killed and the run recorded as a timeout.
CONVERT_PROFILE_ROOTrequired./data/convert-profilesA fresh subdirectory per run, removed afterwards.
RETENTION_DAYSrequired30Documents are deleted after this. Operation rows and hashes remain.
MAX_UPLOAD_MBrequired200Refused above this, with the limit named, rather than failing part way.
REDACTION_VERIFYrequiredstrictStrict means an unverified redaction is never offered. There is no other sensible value.

The build, in order

  1. Import, identity and a preview that warns

    A document's hash, structure and hazards are known before any operation is offered.

    1. Filenames lie; the bytes do not.

      Files app/import.pytests/test_dedupe.py

    2. Encryption, signatures and forms all change what is safe to run.

      Files app/probe.py

    3. Removing somebody's restrictions is a decision, not a side effect.

      Files app/ops/decrypt.pytests/test_encrypted.py

    done when · tick each as it passes
    watch 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.
  2. The structural operations

    Merge, split, reorder, rotate and extract produce new documents and never surprise you.

    1. And show the resulting pages before anything runs.

      Files app/ranges.pytests/test_ranges.py

    2. Test it by filling the merged form, not by reading the spec.

      Files app/ops/merge.pytests/test_merge_forms.py

    3. The input row is never touched, by any code path.

      Files app/ops/base.pytests/test_immutable.py

    done when · tick each as it passes
    watch 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.
  3. Redaction that actually removes

    Text marked for redaction is absent from the output file, proved by extraction rather than by looking.

    1. The rectangle is the last step, not the operation.

      Files app/ops/redact.py

    2. An incremental save keeps the version you were removing.

      Files app/ops/redact.pydocs/redaction.md

    3. Two different checks, because they miss different things.

      Files tests/test_redaction_verified.py

    done when · tick each as it passes
    watch 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.
  4. 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.

    1. Shared profiles are how one bad file poisons the next run.

      Files app/convert.pytests/test_timeout.py

    2. A document should not be able to make outbound requests.

      Files app/sandbox.py

    3. Rendering differences between versions are real and surprising.

      Files app/ops/base.py

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more documents & pdfs.