The NOFO Builder is a Word-2-PDF pipeline that ingests Word files and generates a tagged PDF file using a USWDS-based design that is both accessible and attractive for applicants. It is a tool to build publishable PDFs from reviewed and finalized NOFO documents.
The NOFO Builder is a Django app that can be run as a Python process or as a Docker container.
A "Notice of Financial Opportunity" (NOFO) is a big document accouncing government funding for certain projects or activities (like an RFP). Suppliers can bid on the NOFO for a chance to win funding for delivering the specified outcome.
An example of a NOFO might be an announcement of funding to provide preschool services in Florida.
NOFOs are typically very long, very plain documents without much in the way of formatting.
The SimplerNOFOs project relies on NOFOs that have been written using content guides: essentially, templated starter documents that ensure NOFOs are structured in similar ways.
Once the NOFO documents have been finalized, the NOFO Builder imports these documents as .docx files to generate publishable PDFs that are better structured and easier to read.
NOFOs are written by HHSโ Operating Divisions (OpDivs), and peer-edited by Bloom editing coaches, before proceeding through internal reviews. The writing and editing happens using โcontent guides:โ template-like Word documents that provide a starting point for new NOFOs. Content guides use tagged headings, lists, and tables, and structure the flow of content for a NOFO.
Once a NOFO is reviewed and approved, our workflow is:
- NOFO is approved to be published
- A NOFO designer receives the finalized .docx file
- The NOFO designer logs into the NOFO builder
- The NOFO designer uploads the .docx file to create an HTML representation of the NOFO
- The NOFO designer can view and make edits to the uploaded NOFO
- We use a PDF renderer to output the NOFO as a PDF, based on the HTML layout.
- Done!
The provisional source-native readability integration is documented in Source-native readability metrics.
python is a high-level, general-purpose programming language, popular for programming web applications.
This project uses Python >=3.14.
You can find the exact version of python that is being used by referencing the Dockerfile. If you want to change your Python version, I would recommend pyenv.
Changing Python version
Note: this is not required for initial installation + booting up the app, but I am putting it here so that I remember.
The assumption here is that we are using pyenv to manage our Python version.
# check for currently supported versions of Python
pyenv install --list
# if the version you want is not shown, you can try updating pyenv
brew update && brew upgrade pyenv
# install new version of python with pyenv
pyenv install 3.14.0
# update python versions in various files
# here is a commit that is representative: #538d753a4d961e4d97c783bb7d4157a655ffd12a
# point poetry at the new python
poetry env use 3.14
# refresh lockfile metadata for the new python *without* bumping any packages
poetry lock --no-update
# install exactly from the lockfile
poetry install --syncpoetry is a tool for dependency management and packaging in Python. It allows you to declare the libraries your project depends on and it will manage (install/update) them for you.
Updating dependencies
For full instructions on updating Python dependencies, including routine updates, major version upgrades, and a pre-merge checklist, see Updating Python dependencies.
pre-commit is a framework for managing and maintaining multi-language pre-commit hooks. It helps ensure code quality by running automated checks before each commit. The dependency is included as a poetry dev dependency, so the only local action is to install the pre-commit hooks for this project:
# Install the git hook scripts
poetry run pre-commit install
# Optional: run against all files (not just staged changes)
poetry run pre-commit run --all-filesOur pre-commit configuration includes:
- Black: Python code formatter
- isort: Import sorter
- Django check: Runs Django's system checks
- General hooks: Trailing whitespace, file endings, YAML validation, etc.
The hooks will automatically run on every commit. Files are excluded from formatting if they're in:
- Static files (
nofos/bloom_nofos/static/) - Migration files (
*/migrations/) - SVG files (
.svg) - Certificate files (
.crt)
A docker container allows a developer to package up an application and all of its parts. This means we can build an app in any language, in any stack, and then run it anywhere โ whether locally or on a server.
You will need a .env file to run this application.
# create .env file from example file
cp ./nofos/bloom_nofos/.env.example ./nofos/bloom_nofos/.envIf you are running locally, the example file will work just fine.
Just install the dependencies and boot it up. Pretty slick. ๐
Important: make sure to run poetry commands from the ./nofos directory.
# install dependencies
poetry install
# make sure you are in the "./nofos" directory
cd ./nofos
# run migrations (needed when first booting up the app)
poetry run migrate
# run application in 'dev' mode
# (ie, the server restarts when you save a file)
poetry run startThe app should be running at http://localhost:8000/.
On a Mac, press Control + C to quit the running application.
Currently, the NOFO Builder is an internal tool whose entire purpose is managing and printing NOFO documents, so the user features are pretty barebones. What this means is that we rely on the Django admin for user adminstration.
During first-time setup, create a superuser account.
# create superuser account
poetry run python manage.py createsuperuserSuperusers are the only accounts able to access the admin backend at http://localhost:8000/admin. Once you are logged in, you can use the admin backend to create and manage accounts for new users.
Django's default commands can be run by calling python manage.py {command}. In this repo, we are using poetry to run them.
Important: make sure to run poetry commands from the ./nofos directory.
# running default django commands
poetry run python manage.py {runserver, makemigrations, migrate, etc}This app uses a static version of the US Web Design System (USWDS) styles, downloaded on August 11, 2025. At the time of writing, we are using version 3.13.0.
Updating USWDS
We don't have a frontend build pipeline, so we don't really fit into the model that USWDS describe for getting up and running in their tutorial.
Instead, we link to USWDS built assets in our <head> that we serve from our static folder.
Periodically, we refresh these files with the newer versions so that we bring in the most recent updates.
- Visit downloads page: https://designsystem.digital.gov/download/
- "Download code"
- Copy static assets to Django /static/uswds folder
- Copy
/dist/css/uswds.css - Copy
/dist/js/uswds-init.js - Copy
/dist/js/uswds.js - Move them all into
/nofos/bloom_nofos/static/uswds
- Copy
- Inside of "uswds.css", do a find-replace:
- Find/replace: "../fonts" to
/static/fonts - Find/replace: "../img" to
/static/img
- Find/replace: "../fonts" to
- Copy in new images
- Copy all images in
dist/img/(not subfolders) - Move them to
/nofos/bloom_nofos/static/img
- Copy all images in
- Done!
Well, yes and no. Technically, this is all you need to do, but we don't know if the new version of USWDS creates any layout issues for us. The actual diffs of what changed since the last version of USWDS is too large to meaningfully understand, so we have to do this manaully.
The last step is looking through the app vs a deployed version and checking for differences in layout.
If found, you can decide if the new change is better/equivalent. If not then add CSS to revert the change.
No additional environment variables are needed to run the application in dev mode, but to run in production, several are needed.
To manually deploy to production, create a new file ./nofos/bloom_nofos/.env.production.
-
DEBUG=false: Never run in production with debug mode turned on.- default
True
- default
-
SECRET_KEY: used by Django to encrypt sessions. This can be any random string of sufficient complexity.- default
secret-key-123
- default
-
DATABASE_URL: This app can be configured to use an external database or a local SQLite database. In production, it uses an external Postgres database.- default
"": this means Django will default to using a local SQLite database.
- default
-
DJANGO_ALLOWED_HOSTS: Django will not run correctly on the server unless the domain is specified ahead of time. This env var can contain 1 domain or a comma-separated list of domains- default
"": no effect unless Django is running in production.
- default
-
DOCRAPTOR_API_KEY: Our API key for printing documents using DocRaptor.- default
"YOUR_API_KEY_HERE": this key works for printing test documents (with a watermark)
- default
-
DOCRAPTOR_IPS: IP addresses that we expect DocRaptor requests to come from. Note that these can be overridden.- default
"": this means zero IPs are safelisted
- default
-
GRABZIT_APPLICATION_KEYandGRABZIT_APPLICATION_SECRET: credentials for the GrabzIt Word conversion provider. Do not share production credentials with another environment.- default
"": Word export is unavailable without both values. - Store values only in the approved secret manager. Never commit, log, screenshot, or include them in issues or pull requests.
- default
-
GRABZIT_WORD_EXPORT_ALLOWED_HOSTS: exact comma-separated hostnames allowed to use the configured GrabzIt credentials. The application enforces this on the server before writing provider-side cookies or requesting a conversion.- default
"nofos.simpler.grants.gov": production remains available; development, training, grantee, and unknown hosts fail closed. Set this explicitly to an empty string outside production until that environment has dedicated credentials.
- default
-
API_TOKEN: Bearer token to allow API access.- default
"": this will block any and all API access.
- default
# build an image locally
docker build -t pcraig3/bloom-nofos:{TAG} .
# run the container
docker run -it -p 8000:8000 pcraig3/bloom-nofos:{TAG}The container should be running at http://localhost:8000/.
On a Mac, press Control + C to quit the running docker container.
Building a container on an M1 Mac to deploy on a cloud environment means targeting amd64 architecture.
# build the container
- docker buildx build --platform linux/amd64 --build-arg IS_PROD_ARG=1 -t gcr.io/{SERVICE}/{PROJECT}:{TAG} .
# push the container
- docker push gcr.io/{SERVICE}/{PROJECT}:{TAG}
# deploy the container
- gcloud run deploy {SERVICE} \
--project {PROJECT} \
--platform managed \
--region {REGION} \
--image gcr.io/{SERVICE}/{PROJECT}:{TAG} \
--add-cloudsql-instances {SERVICE}:{REGION}:{PROJECT} \
--allow-unauthenticated- DEPLOYMENT.md โ Deployment & contribution workflow.
- Builder usage & quality metrics โ Dashboard access, production setup, OpDiv filtering, and historical data.
- Groups โ how user and NOFO groups work, and how to add a new group.
- Import rules โ every automatic content rule applied when a NOFO is imported.