This example turns the local Dynamic Web TWAIN Service into a shared, network-accessible scanning gateway.
It adds three controls that are missing from a direct LAN-exposed scanner:
- registered user access with password-based sign-in
- JWT-protected API calls for every scan request
- scanner locking so only one user can acquire a device at a time while other users can still see status
document-remote-scan-python-web-twain.mp4
- Hidden-by-default registration and sign-in flows on the public landing view
- Self-service user registration backed by SQLite
- OAuth2 password flow with JWT bearer tokens
- Built-in administrator portal for listing and deleting registered users
- Same-origin web dashboard served by FastAPI
- Explicit scanner lease table with lock owner and expiry tracking
- TWAIN scanner discovery over the local network through the Python SDK
- Centered scan preview with scrollable page viewing and thumbnail navigation
- Scan preview streaming as PNG or JPEG pages
- Export scanned results as PDF or PNG files after each scan
- Python 3.10+
- Dynamic Web TWAIN Service running on the machine attached to the scanner
- A license key that includes the REST API module
Copy .env.example and set the required values:
Copy-Item .env.example .envThe app reads webexample/.env automatically on startup. Restart the FastAPI process after changing .env so the updated values are applied.
Environment variables:
DWT_LICENSE_KEY: Dynamic Web TWAIN license keyDWT_SERVICE_HOST: REST API host, for examplehttp://192.168.1.20:18622REMOTE_SCAN_JWT_SECRET: long random secret used to sign access tokensACCESS_TOKEN_TTL_MINUTES: bearer token lifetimeSCANNER_LOCK_TTL_SECONDS: stale lock timeoutREMOTE_SCAN_SCANNER_TYPES: scanner type mask.0x50enables TWAIN and TWAIN x64.REMOTE_SCAN_ADMIN_USERNAME: optional bootstrap administrator usernameREMOTE_SCAN_ADMIN_PASSWORD: optional bootstrap administrator passwordREMOTE_SCAN_ADMIN_FULL_NAME: display name for the bootstrap administrator account
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
Get-Content .env | ForEach-Object {
if ($_ -and -not $_.StartsWith('#')) {
$name, $value = $_.Split('=', 2)
Set-Item -Path Env:$name -Value $value
}
}
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Open http://127.0.0.1:8000 in a browser on any machine in the same network.
- If
REMOTE_SCAN_ADMIN_USERNAMEandREMOTE_SCAN_ADMIN_PASSWORDare set, the app creates or refreshes that administrator account on startup. - If no bootstrap admin is configured, the first registered account becomes the administrator automatically.
- The landing page shows which account currently has administrator access.
- Administrators can review and delete registered users from the
Registered Userspanel after signing in. - End users still register from the public landing page, but the registration and sign-in forms stay hidden until a user clicks the corresponding button.
- Scan results render in a centered preview stage with a scrollable container and page thumbnails.
Export PDFdownloads a single PDF containing all captured pages.Export PNGdownloads one PNG for a single-page scan, or a ZIP archive of PNG files for multi-page scans.
- A signed-in user requests a scan for a specific scanner.
- The gateway writes a scanner lease to SQLite before it starts the job.
- Other users can still refresh the dashboard and see who owns the device.
- The gateway creates a pending Dynamic Web TWAIN job, starts it, streams the pages back, then releases the lease.
- If a client disconnects or a job fails, the lease automatically expires based on
SCANNER_LOCK_TTL_SECONDS.
- Keep the FastAPI app and the Dynamic Web TWAIN Service behind HTTPS in production.
- Replace the JWT secret before exposing the gateway outside a trusted network.
- The frontend stores the access token in
sessionStorage, which reduces persistence across browser restarts. - The app adds CSP, frame, and MIME-sniffing headers to reduce common browser-side risks.