Guide version 1.10.0 ยท For Sequences 0.66.0
Installation guide
How your IT staff run Sequences on a server in your own network or in your AWS account (beta): the container, the license file, a snapshot, sign-in with your own provider and upgrades.
What you need
Sequences comes as one container image that holds the search service and the programs it runs: NCBI BLAST+ 2.17.0, FASTA36 36.3.8i, MAFFT 7.505, HMMER 3.4 with the antibody numbering models, and Node.js 22.23.5. Queries, results and the sequence snapshot stay on your server, apart from two features that stay off until you turn them on: watch emails, which send each watch's name and its new matches or publications to the person's address through Resend, and claim suggestions, which send a case's claims, with the paragraphs of the description chosen for them, to a model you run. Otherwise, with sign-in on, the only connection the service opens is to your sign-in provider. See Network traffic.
- A Linux server (x86-64) with Docker Engine. For reference, the hosted service runs with 12 CPUs and 56 GiB of memory for the November 5, 2026 snapshot.
- Disk for the data volume: the snapshot (38 GiB for the November 5, 2026 build) and room for results, up to 8 GiB for one search unless you set less, and about 250 MB for a portfolio export at its limit. Loading a copy also needs room for the copy in an import folder, and a build like the November 5, 2026 one needs at least 110 GiB free in the volume. Docker keeps volumes under
/var/lib/docker/volumesunless you set it up otherwise, so check that disk. See Get a snapshot. - The Sequences image, a file you load with
docker load, and a license file, both from ConductScience. To get them, write to support@conductscience.com. - A snapshot: a copy with its SHA-256 list, or the public source files to build one.
- For sign-in: an OpenID Connect provider (Okta, Microsoft Entra ID, Keycloak or another), a host name with a TLS certificate, and a reverse proxy on the same server that serves HTTPS on that name.
- If you like, PubMed's files and the Retraction Watch Database for paper citations and marks, about 75 GB to download, PubTator 3's file of about 6 GB for a case's paper sentence, and ROR's data file for applicant suggestions. See Paper citations and marks and ROR suggestions.
- If you like, a language model on a server you control, for claim suggestions. See Claim suggestions from a model.
- If you like, the clinical context folder, a copy with its SHA-256 list from ConductScience, for the drugs, diseases and trials of a protein hit's target. See Clinical context.
- If you like, the funding trail folder and its SHA-256 list, from ConductScience. It holds the NIH awards and other grants of the papers listed under hits, and can hold the statements on federally sponsored research in US published applications. See Funding trail.
The commands below use <version> for the image's version, such as 0.43.1. They keep the service's files in two places: a Docker volume named sequences-data for data, and /etc/sequences on the server for settings, which the container reads at /config.
In your organization's own AWS account, the AWS install kit does these steps for you. It is a beta. See In your AWS account (beta).
Load the image
docker load -i sequences-<version>.tar
docker run --rm conductscience/sequences:<version> versions
versions prints the release and the version of each search program. The service runs as user ID 10001 (sequences), not as root, and keeps everything it writes in /data. Only the backup and restore commands under Backups run the image as root.
The license file
The service needs a license file signed by ConductScience. The image checks the signature with ConductScience's public license key, which is built in, so there is no license server. The license names your organization, your site (a label for where it runs, such as "Main data center"), the plan (such as trial) and its first and last day. Both days count, in UTC. It names no computer, so it keeps working when you move the service to another server.
sudo install -d -m 0755 /etc/sequences
sudo install -m 0644 license.json /etc/sequences/license.json
docker run --rm -v /etc/sequences:/config:ro conductscience/sequences:<version> check-license
check-license prints what the file allows, and exits with 1 if it does not allow new searches today. From 60 days before the last day, it and the page say when the license ends. The page's footer names the plan and the last day, such as "Trial license, ends 2026-12-30", and so does the usage page.
An evaluation license, with the plan evaluation, lets your organization try the server edition before it buys. It runs the same service as any license: every feature, on as many servers and for as many people as you choose, because a license names no computer and counts no accounts. The footer and the usage page read "Evaluation license, ends" and its last day. When it ends, the server turns read-only, as after a trial: people can still open, mark and export their results, and new work needs a new license file. How long it runs, and what comes with it, are set in its terms.
At start, a license file that is missing or cannot be used stops the service, and the log says why. A license can give grace days: days after the last day on which new work still runs, while the page says when it stops.
Before the first day, and after the last day and any grace days, the service runs but refuses new work with a message that says why. That covers new searches of every kind, run again, alignments, antibody numbering, listing uploads, saving and editing sequences, creating or changing cases and their plans, making or resuming a watch, and portfolio exports. Watches stop running. People can still open, mark and export the results they have, download a case's report and a finished portfolio export, pause a watch, cancel a search and delete their own files. Put a renewed file in the same place: the service reads it again on the next request, with no restart. If the file goes missing or cannot be used while the service runs, new work pauses in the same way.
Get a snapshot
Searches run on a snapshot: a read-only folder built from public sequence files, with each sequence stored once with every record and publication that carries it. A snapshot's ID is its build date and a short hash, such as 2026-11-05-c456f6bdba29. A delta adds newer records to a snapshot without a rebuild. People see each delta as an addition: Collection on the search page offers Main plus New, and each watch runs again within the hour. In the data volume a snapshot lives in corpora/<snapshotId>/ and its deltas in deltas/<snapshotId>/<deltaId>/. Never change files there: the service checks them by their hashes.
There are two ways to get one: load a copy, or build one from the public files.
Load a copy
A copy comes as the snapshot folder and its SHA-256 list: one line for each file in the folder, with the SHA-256, two spaces and the path inside the folder. Put both in an import folder on the server:
/srv/sequences/import/2026-11-05-c456f6bdba29/
/srv/sequences/import/2026-11-05-c456f6bdba29.SHA256SUMS
The service reads the folder as user ID 10001, so make it readable by everyone:
sudo chmod -R a+rX /srv/sequences/import
You can check the files yourself first, with the standard tool:
cd /srv/sequences/import/2026-11-05-c456f6bdba29
sha256sum --check --quiet ../2026-11-05-c456f6bdba29.SHA256SUMS
Then load it into the data volume, which is made once and keeps everything the service stores:
docker volume create sequences-data
docker run --rm \
-v sequences-data:/data \
-v /srv/sequences/import:/import:ro \
conductscience/sequences:<version> \
load /import/2026-11-05-c456f6bdba29 --sums /import/2026-11-05-c456f6bdba29.SHA256SUMS
load stops at the first problem:
- It checks
manifest.jsonagainst the list, and that the folder holds exactly the files the list names. - It copies each file into a hidden folder in the data volume, checking its SHA-256 as it copies.
- It runs the snapshot's own checks: its counts, its hashes and its BLAST databases.
- It makes the folder read-only and gives it its ID. Only then does the service list it.
If a step fails, the copy is removed and the message names the file or check that failed. A running service lists a new snapshot on the next page load, so loading needs no restart. Loading a snapshot that is already there is refused. A delta also comes as a folder and its SHA-256 list, and loads the same way, after its snapshot and every delta before it. The import folder and the data volume each need the size of the snapshot while it loads, and you can delete the import copy afterwards.
Sources and terms
Each source keeps its own terms, and the UniProt Consortium asks that its copyright statement goes with every copy of UniProt data. Each snapshot and delta carries ATTRIBUTION.txt: who supplies each source, the release, the terms page and the notices, including UniProt's statement word for word. The search page shows the same text under Sources and terms. If a snapshot folder has no ATTRIBUTION.txt, as in snapshots built before it existed, print the notice and keep it with your copy of the snapshot:
docker run --rm -v sequences-data:/data conductscience/sequences:<version> \
attribution 2026-11-05-c456f6bdba29 > 2026-11-05-c456f6bdba29.ATTRIBUTION.txt
Build from the public files
fetch downloads the public files and build turns them into a snapshot. fetch is the only command that downloads anything. It reads from https://ftp.ncbi.nlm.nih.gov/genbank/, https://ftp.ebi.ac.uk/pub/databases/ and https://ftp.uniprot.org/pub/databases/uniprot/. GenBank publishes only its current release, so fetch needs that release number:
curl -s https://ftp.ncbi.nlm.nih.gov/genbank/GB_Release_Number
With the number it prints, such as 274.0:
docker run --rm -v sequences-data:/data conductscience/sequences:<version> \
fetch --source all --release 274.0 --out /data/inputs/release-274.0
docker run --rm -e UV_THREADPOOL_SIZE=10 -v sequences-data:/data conductscience/sequences:<version> \
build --inputs /data/inputs/release-274.0 --title "Public patent sequences" --workers 8
--source allfetches the patent files and Swiss-Prot. TrEMBL and the ESTs are large, soallleaves them out. To fetch them too, add--source uniprot-trembl --source ena-estto the samefetchcommand.fetchopens at most four connections to one server, records each file's size and SHA-256, and picks up where it stopped if you run it again after an interruption.buildchecks every file against that record, then parses, merges and indexes them, builds the BLAST databases, runs its checks and seals the snapshot. It prints the snapshot ID at the end.- Set
UV_THREADPOOL_SIZEto the number of workers plus two.--resumerestarts a failed build after its last finished stage.
As a guide, from the November 5, 2026 build of the patent sets and Swiss-Prot: the source files take about 25 GB, the build needs up to 79 GiB more working space in the data volume while it runs, and the finished snapshot keeps 38 GiB. Before you run fetch, leave at least 110 GiB free in the volume, which covers the source files and the build together. When the snapshot is built, the source files can go:
docker run --rm -v sequences-data:/data --entrypoint rm conductscience/sequences:<version> \
-rf /data/inputs/release-274.0
Check a snapshot again
docker run --rm -v sequences-data:/data conductscience/sequences:<version> verify 2026-11-05-c456f6bdba29
docker run --rm -v sequences-data:/data conductscience/sequences:<version> deltas 2026-11-05-c456f6bdba29
verify runs the same checks as the load, and exits with 1 if any fails. deltas lists a snapshot's deltas in order. sums <snapshotId> prints the folder's SHA-256 list, to compare with the list that came with the copy.
Start the service
Sign-in is on when BIOSEQUENCE_ALLOWED_HOSTS is set, and off when it is not. Choose one of the two ways to run the service. Both use the sequences-data volume with a snapshot in it and the license file in /etc/sequences.
| What | Sign-in off | Sign-in on |
|---|---|---|
| Who uses it | One person, or a test, on one Linux machine: the local workbench | People in your organization, through your sign-in provider |
| Where it answers | 127.0.0.1 of that machine only | Your host name, through your HTTPS reverse proxy |
| Jobs | One shared list. Anyone who can reach port 8765 on that machine, such as another local user or an SSH tunnel, can open every search | Each person's searches belong to their account, and nobody else can open them |
| Collections | Snapshot sets, and FASTA collections imported on the page | Snapshot sets, with no imports |
Sign-in off: one Linux machine
docker run -d --name sequences --restart unless-stopped \
--network host \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-v sequences-data:/data \
-v /etc/sequences:/config:ro \
conductscience/sequences:<version>
Open http://127.0.0.1:8765/biosequence on that machine (http://localhost:8765/biosequence works too). From another computer, tunnel the port over SSH and open the same address there:
ssh -L 8765:127.0.0.1:8765 <user>@<server>
With sign-in off the service listens on 127.0.0.1 only and answers only requests addressed to 127.0.0.1 or localhost. --network host puts that address on the server itself, since a published port (-p) cannot reach a service that listens on the container's own loopback. This is for Linux servers. Docker Desktop on Windows and macOS handles host networking differently, and this guide does not cover it.
Sign-in on: your organization
You need a host name, such as sequences.example.org, its TLS certificate, and a reverse proxy on the same server that serves HTTPS on that name.
1. Register Sequences with your provider
Register a web application (a confidential client) that uses the authorization code flow:
- Redirect URI:
https://sequences.example.org/api/auth/callback - Scopes:
openid email profile - Client authentication: a client secret
Note the issuer URL, the client ID and the client secret. Provider settings shows where each one is.
2. Write the settings
Save the client secret in its own file, readable only by root and by group 10001, the service's group. read asks for the secret without showing it, so it stays out of your shell history:
sudo install -m 0640 -g 10001 /dev/null /etc/sequences/oidc-client-secret
read -rs SECRET && printf %s "$SECRET" | sudo tee /etc/sequences/oidc-client-secret > /dev/null; unset SECRET
Then write the settings in /etc/sequences/sequences.env:
BIOSEQUENCE_ALLOWED_HOSTS=sequences.example.org
BIOSEQUENCE_OIDC_ISSUER=<issuer URL>
BIOSEQUENCE_OIDC_CLIENT_ID=<client ID>
BIOSEQUENCE_OIDC_CLIENT_SECRET_FILE=/config/oidc-client-secret
BIOSEQUENCE_OIDC_NAME=your Example Org account
BIOSEQUENCE_ALLOWED_DOMAINS=example.org
Choose who may sign in with at least one of these:
BIOSEQUENCE_INVITE_FILE=/config/invite.txt: a file with one person per line, as the user claim names them (their email address by default), where#starts a comment. The service reads the file again whenever it changes and checks it on every request, so removing a line ends that person's access at their next click.BIOSEQUENCE_ALLOWED_DOMAINS: everyone whose user claim ends in@and one of these domains.BIOSEQUENCE_OIDC_REQUIRED_GROUP: only people whose group claim holds this value. The group claim is read at sign-in, so a change at the provider applies at the next sign-in, withinBIOSEQUENCE_SESSION_HOURS(12 by default).
A person may sign in when the invite file or an allowed domain names them, if you set either, and their group claim holds the required group, if you set one. With only a required group, everyone in that group may sign in. To let someone see the usage page, add them to BIOSEQUENCE_ADMINS as the user claim names them, separated by commas, such as BIOSEQUENCE_ADMINS=lab.manager@example.org. They must still be allowed to sign in. See Usage.
- With
BIOSEQUENCE_OIDC_USER_CLAIMset to anything butemail, also setBIOSEQUENCE_OIDC_REQUIRED_GROUP. The service checks that an email address is verified, but it has no way to check another claim, such as a sign-in name a user can change at some providers. - A user value with letters beyond plain ASCII, such as an accented name, never matches the invite file or an allowed domain. To let such people in, use the required group alone, with no invite file and no allowed domains.
- With more than one name in
BIOSEQUENCE_ALLOWED_HOSTS, sign-in happens at one address only:BIOSEQUENCE_SITE_URL, or the first name in the list. A visit on another name goes to that address first. Register only that address's redirect URI. - If your provider's certificate comes from your own certificate authority, put the authority's PEM file in
/etc/sequences/ca.pemand addNODE_EXTRA_CA_CERTS=/config/ca.pemto the settings.
3. Start the container
If you started the sign-in-off container, remove it first: docker stop sequences && docker rm sequences. Then:
docker run -d --name sequences --restart unless-stopped \
--read-only --tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-p 127.0.0.1:8765:8765 \
-v sequences-data:/data \
-v /etc/sequences:/config:ro \
--env-file /etc/sequences/sequences.env \
conductscience/sequences:<version>
docker logs sequences
The log names the host, the issuer and the license, for example:
Sequences for sequences.example.org, signed in through https://login.example.org/realms/example, on 0.0.0.0:8765
Trial license for Example Org, 2026-12-01 to 2027-01-15
Inside the container the service listens on all of its addresses (BIOSEQUENCE_HOSTED_BIND=0.0.0.0), and -p 127.0.0.1:8765:8765 publishes that port on the server's loopback address only. Keep the 127.0.0.1: part, so that only the reverse proxy reaches the port. The service answers only requests that name an allowed host. A request addressed to 127.0.0.1 works only from inside the container, which is how the image's health check reaches it. The one exception is GET /api/health, which also answers at an IP address, for a health check that cannot name your host. With this command, such a check runs on the server itself, at http://127.0.0.1:8765/api/health. A load balancer on another machine needs the port published on an address it can reach, as the AWS kit publishes it on the server's private address. See Health and logs.
4. Point the reverse proxy at it
The proxy serves HTTPS on your host name, passes the Host header through unchanged, and forwards to http://127.0.0.1:8765. With Caddy, which keeps the Host header and can get its own certificate for a name that is reachable from the internet:
sequences.example.org {
reverse_proxy 127.0.0.1:8765
}
For an internal name, give Caddy your certificate with a tls line in the same block, such as tls /etc/ssl/sequences.example.org.crt /etc/ssl/sequences.example.org.key.
With nginx, where a large sequence listing or result file needs a large upload limit:
server {
listen 443 ssl;
server_name sequences.example.org;
ssl_certificate /etc/ssl/sequences.example.org.crt;
ssl_certificate_key /etc/ssl/sequences.example.org.key;
client_max_body_size 2g;
proxy_request_buffering off;
location / {
proxy_pass http://127.0.0.1:8765;
proxy_set_header Host $host;
}
}
Open https://sequences.example.org/ and sign in.
What sign-in checks
The service uses the authorization code flow with PKCE (S256), a one-time state and a nonce. It reads the provider's discovery document and keys, checks the ID token's signature (RS256, PS256, ES256 and EdDSA families, never none or a shared secret), its issuer, audience, expiry and nonce, and asks the userinfo endpoint for claims the ID token leaves out. With BIOSEQUENCE_OIDC_USER_CLAIM=email, the provider must mark the email as verified. Sessions are signed cookies (__Host-, Secure, HttpOnly, SameSite=Lax) that last BIOSEQUENCE_SESSION_HOURS. Each search belongs to the account's subject ID at the provider, and any other account gets "not found" for it.
Signing out removes the session from that browser. It does not sign the person out of the provider. Removing someone from the invite file ends their access at their next click. To end every session at once, for example when someone leaves and the rule that let them in is a domain or a group, replace the cookie key and restart, and everyone signs in again:
docker exec sequences rm /data/session-secret
docker restart sequences
If you set BIOSEQUENCE_SESSION_SECRET_FILE, put a new key of 32 characters or more in that file instead, then restart. A new cookie key does not end API tokens: revoke them on the usage page.
Move to another provider
Each search belongs to the user ID (sub) that your provider gave its owner, and another provider's IDs name other people. So the data volume keeps the provider it first signed in through: the first start with sign-in on writes the issuer to /data/sign-in-provider.json, and a later start with a different BIOSEQUENCE_OIDC_ISSUER stops with a message that names both. To move on purpose, choose one:
- Start fresh. Use a new data volume with the new provider and load the snapshot into it. Keep the old volume if people need their old searches, and open it with the old provider.
- Keep the volume. Stop and remove the container, remove the record with the commands below, put the new provider's settings in
/etc/sequences/sequences.envand start the container as in step 3. Old searches stay on the volume under the old user IDs. If the new provider gives people the same user IDs (sub), for example because it reads the same user directory, they keep their searches. Otherwise nobody can open the old ones.
docker stop sequences && docker rm sequences
docker run --rm -v sequences-data:/data --entrypoint rm conductscience/sequences:<version> /data/sign-in-provider.json
API tokens for programs
A program, such as a script your search team runs, signs in with a personal API token instead of a browser. A person signed in with the browser opens API tokens beside their account, names the token, picks how long it lasts and copies it. The page shows it once. The program sends it on every request:
curl -H "Authorization: Bearer <token>" https://sequences.example.org/api/jobs
https://sequences.example.org/api/openapi.json describes the routes a program can use, in OpenAPI 3.1. It is open without sign-in and holds no data. POST /api/batches starts up to 100 searches in one request.
- A token acts as the person who made it, under the same rules, checked on every request. Taking someone off the invite file, or their domain off
BIOSEQUENCE_ALLOWED_DOMAINS, stops their tokens at the program's next request. - The group claim is read only at a browser sign-in. With
BIOSEQUENCE_OIDC_REQUIRED_GROUPset, a token keeps the groups its owner had when it was made, so when someone leaves the group, revoke their tokens on the usage page. - A token lasts at most
BIOSEQUENCE_TOKEN_DAYSdays, 90 by default, and each person may have 10 at a time. The service keeps only a SHA-256 hash of each token, in/data/tokens.json, so nobody can read one back. - A token cannot make another token: that needs the browser.
BIOSEQUENCE_TOKEN_DAYS=0turns tokens off and stops every token at once. Turned on again, tokens that have not expired work again, so to end tokens for good, revoke them.- A request with a token skips the browser's cross-site checks, which protect the cookie a browser sends by itself. It still has to name an allowed host.
With sign-in off there are no tokens: a program on the same machine calls http://127.0.0.1:8765 as the page does.
Provider settings
Okta
- In the Okta admin console, go to Applications, Create App Integration, and choose OIDC, OpenID Connect and Web Application.
- Grant type: Authorization Code. Sign-in redirect URI:
https://sequences.example.org/api/auth/callback. - Copy the Client ID and Client secret from the application's General tab.
- To limit access to a group, open Security, API, the
defaultauthorization server and Claims, and add a claim namedgroupsto the ID token, of value type Groups, with a filter that matches your group.
BIOSEQUENCE_OIDC_ISSUER=https://example.okta.com/oauth2/default
BIOSEQUENCE_OIDC_CLIENT_ID=<client ID>
BIOSEQUENCE_OIDC_CLIENT_SECRET_FILE=/config/oidc-client-secret
BIOSEQUENCE_OIDC_NAME=your Example Org Okta account
BIOSEQUENCE_OIDC_REQUIRED_GROUP=Sequences Users
The issuer of the default authorization server ends in /oauth2/default. With Okta's org authorization server it is the Okta domain alone, such as https://example.okta.com.
Microsoft Entra ID
- In the Microsoft Entra admin center, go to App registrations and New registration. Add a Web redirect URI:
https://sequences.example.org/api/auth/callback. - Under Certificates & secrets, add a client secret and copy its value.
- To limit access, add an app role under App roles, such as
Sequences.User, then assign people or groups to it under Enterprise applications, the application, Users and groups. Turning on Assignment required there also stops everyone else at the Microsoft sign-in.
Entra ID tokens carry no email_verified claim, so name people by their sign-in name instead:
BIOSEQUENCE_OIDC_ISSUER=https://login.microsoftonline.com/<tenant ID>/v2.0
BIOSEQUENCE_OIDC_CLIENT_ID=<application (client) ID>
BIOSEQUENCE_OIDC_CLIENT_SECRET_FILE=/config/oidc-client-secret
BIOSEQUENCE_OIDC_CLIENT_AUTH=client_secret_post
BIOSEQUENCE_OIDC_NAME=your Example Org Microsoft account
BIOSEQUENCE_OIDC_USER_CLAIM=preferred_username
BIOSEQUENCE_OIDC_GROUP_CLAIM=roles
BIOSEQUENCE_OIDC_REQUIRED_GROUP=Sequences.User
Keycloak
- In your realm, go to Clients and Create client: type OpenID Connect, client ID
sequences. - Turn Client authentication on and keep Standard flow. Valid redirect URI:
https://sequences.example.org/api/auth/callback. - Copy the secret from the client's Credentials tab.
- To limit access to a group, open the client's Client scopes tab, then
sequences-dedicated, Add mapper, By configuration and Group Membership. Set the token claim name togroups, with Add to ID token on. With Full group path on, the value is the group's path, such as/sequences-users.
BIOSEQUENCE_OIDC_ISSUER=https://keycloak.example.org/realms/<realm>
BIOSEQUENCE_OIDC_CLIENT_ID=sequences
BIOSEQUENCE_OIDC_CLIENT_SECRET_FILE=/config/oidc-client-secret
BIOSEQUENCE_OIDC_NAME=your Example Org account
BIOSEQUENCE_OIDC_REQUIRED_GROUP=/sequences-users
Keycloak sends email_verified from each user's profile, so people need a verified email address, or set BIOSEQUENCE_OIDC_USER_CLAIM=preferred_username.
Other providers
Any provider that publishes an OpenID Connect discovery document at <issuer>/.well-known/openid-configuration and supports the authorization code flow works with the same settings. If the provider sends the user claim or the groups only from its userinfo endpoint, the service asks for them there.
In your AWS account (beta)
The AWS install kit does the steps of this guide for you in your organization's own AWS account. Kit 1.0.0 is a beta: ConductScience has tested it against stand-ins for the AWS services and has not yet installed it in an AWS account, so try it in a test account first. A later kit drops "beta" once an install in an AWS account passes the kit's checks, and it may change the parameters. The kit runs Sequences 0.43.0 and later.
The kit is three files from ConductScience: a CloudFormation template, kit.sh, the script the server runs, and the kit's own guide, for your AWS administrator. The kit's guide gives every step: what goes where in your bucket, the secrets, the network the server needs, each parameter, updates, backups and removal. One stack made from the template creates:
- One EC2 server, memory-optimized, with no public address and no key pair. The default,
r6i.2xlarge, has 8 vCPUs and 64 GiB of memory, and the service gets all of it but 8 GiB. - Encrypted gp3 disks: a 30 GiB root disk, and a data volume of 200 GiB by default for the snapshots and everything the service stores.
- Security groups, and a role for the server. It can read only its folder of your S3 bucket, the stack's settings and the secrets you name, and with Amazon ECR pull from one repository. Policies you name add to it.
- With sign-in, an HTTPS Application Load Balancer with your ACM certificate. It is internal by default, so only your network reaches it.
On its first boot the server installs Docker, loads the Sequences image from your bucket or from Amazon ECR, copies each snapshot from the bucket, checks every file against the snapshot's SHA-256 list and runs verify on it, then starts the service. The stack is complete only when the service runs. It waits up to 12 hours for the first boot, which takes longer for a larger snapshot. If a step fails, the service does not start and the stack fails, and the server's console says why. You provide the network (the VPC, its subnets, and a NAT gateway or VPC endpoints), the bucket, the secrets, the certificate, the DNS record and the registration at your sign-in provider.
People reach Sequences in one of two ways:
- Through the load balancer, the default: people open your host name and sign in with your OpenID Connect provider.
- Through port forwarding, for a test or one person: sign-in is off, and you open the page through a Session Manager port forward from your computer. Anyone allowed to start a Session Manager session on the server can open Sequences. With sign-in off there are no accounts, so they all see the same saved work.
You manage the server through AWS Systems Manager Session Manager. Nothing can reach its port 22. To change the install, such as to add a delta or move to a new release, put any new files in the bucket, update the stack, then run kit.sh apply on the server in a Session Manager shell, which puts the update in place. A later kit goes in the same way, with its new template and kit.sh. kit.sh check runs in one go the checks this guide gives one at a time: versions, check-license, the health checks and verify. Back up the data volume with EBS snapshots, through AWS Backup or Amazon Data Lifecycle Manager.
Kit 1.0.0 sets only the settings its parameters name, so an install made with it has no paper citations and marks, ROR suggestions, watch emails, claim suggestions or add-ons. ConductScience has no access to your account, and the server sends nothing to ConductScience. To get the kit, write to support@conductscience.com.
Settings
Set these with -e NAME=value or in an env file (--env-file). Docker reads an env file literally: write NAME=value with no quotes. A changed setting takes effect only in a new container: run docker stop sequences && docker rm sequences, then your docker run command again, with the new -e value or the edited env file. docker restart keeps the old settings.
| Setting | Default in the image | What it does |
|---|---|---|
BIOSEQUENCE_LICENSE_FILE | /config/license.json | The license file |
BIOSEQUENCE_DATA_DIR | /data | Snapshots, jobs and results. Keep it on the sequences-data volume |
BIOSEQUENCE_PORT | 8765 | The port the service listens on, 1024 to 65535 |
BIOSEQUENCE_HOSTED_BIND | 0.0.0.0 | The address inside the container with sign-in on. With sign-in off the service uses 127.0.0.1 whatever this says |
BIOSEQUENCE_ALLOWED_HOSTS | none | The host name people use, such as sequences.example.org. Setting it turns sign-in on. Separate several names with commas |
BIOSEQUENCE_SITE_URL | https:// and the first allowed host | The address people sign in at: https:// and one of the allowed hosts, with no port or path. Its /api/auth/callback is the redirect URI you register with your provider |
BIOSEQUENCE_OIDC_ISSUER | none | Your provider's issuer URL |
BIOSEQUENCE_OIDC_CLIENT_ID | none | The client ID of the Sequences application at your provider |
BIOSEQUENCE_OIDC_CLIENT_SECRET_FILE | none | A file that holds the client secret, such as /config/oidc-client-secret. BIOSEQUENCE_OIDC_CLIENT_SECRET takes the secret itself instead |
BIOSEQUENCE_OIDC_CLIENT_AUTH | client_secret_basic | How the service sends the secret: client_secret_basic or client_secret_post |
BIOSEQUENCE_OIDC_NAME | your organization's account | Shown on the sign-in button: "Sign in with your organization's account" |
BIOSEQUENCE_OIDC_SCOPES | openid email profile | Scopes the service asks for. Must include openid |
BIOSEQUENCE_OIDC_USER_CLAIM | email | The claim that names a person. With email, the provider must also send email_verified as true |
BIOSEQUENCE_OIDC_GROUP_CLAIM | groups | The claim that lists a person's groups or roles |
BIOSEQUENCE_OIDC_REQUIRED_GROUP | none | Only people with this value in the group claim may sign in |
BIOSEQUENCE_ALLOWED_DOMAINS | none | Domains that may sign in, such as example.org, separated by commas: people whose user claim ends in @ and one of them |
BIOSEQUENCE_INVITE_FILE | none | A file with one user per line (the user claim's value), where # starts a comment |
BIOSEQUENCE_ADMINS | none | People who see the usage page at /admin, as the user claim names them, separated by commas. They must still be allowed to sign in |
BIOSEQUENCE_SESSION_HOURS | 12 | How long a sign-in lasts, 1 to 168 hours |
BIOSEQUENCE_SESSION_SECRET_FILE | /data/session-secret, made on first start | The key that signs session cookies, 32 characters or more |
BIOSEQUENCE_TOKEN_DAYS | 90 | The longest an API token may last, 1 to 90 days. 0 turns API tokens off. See API tokens for programs |
BIOSEQUENCE_THREADS | CPU count minus 2, at most 14 | Threads for one search, 1 to 14 |
BIOSEQUENCE_CONCURRENCY | 1 | How many searches of the snapshot, exact alignments and portfolio exports run at once, 1 to 16. The others wait their turn |
BIOSEQUENCE_MAX_JOBS | 8 | How many searches the queue holds at once, waiting and running, 1 to 256. A case's run uses the free places and leaves 2 for searches people start from the page |
BIOSEQUENCE_JOB_SECONDS | 3600 | Time limit for one search or portfolio export, 1 to 86,400 seconds |
BIOSEQUENCE_MAX_OUTPUT_BYTES | 8 GiB | Largest result file of one search: a whole number of bytes, with no unit, from 1 KiB to 4 TiB |
BIOSEQUENCE_LISTING_BYTES | 256 MiB | Largest sequence listing file people can upload: a whole number of bytes, with no unit, from 1 MiB to 64 GiB. Keep your proxy's upload limit above it |
BIOSEQUENCE_PATENT_LINK | https://patents.google.com/patent/{office}{number}{kind} | The link on each patent in the results. An empty value removes it |
BIOSEQUENCE_MAIL_FROM | none | The sender of watch emails, such as Sequences <noreply@example.org>. Set it with BIOSEQUENCE_RESEND_KEY, or neither. See Watch emails |
BIOSEQUENCE_RESEND_KEY | none | The API key of your account at Resend, the email delivery service that sends watch emails. Without both mail settings, watches show what they find on the page only |
BIOSEQUENCE_CLAIMS_MODEL_URL | none | The address a case's claims go to when someone asks a language model for features the rules missed, with the whole path, such as http://127.0.0.1:8000/v1/chat/completions. Unset, suggestions are off. See Claim suggestions from a model and Network traffic |
BIOSEQUENCE_CLAIMS_MODEL_API | sequences | The format of the call. openai-chat posts a chat completions call, the format vLLM, llama.cpp's server and Ollama take: the instructions as the system message and the claims as JSON in the user message, at temperature 0, asking for JSON back. sequences posts the service's own JSON request and reads a JSON answer, for a service of your own in front of a model; for that format, write to support@conductscience.com |
BIOSEQUENCE_CLAIMS_MODEL | none | The model name the service puts in each request, up to 200 characters. Needed with the address |
BIOSEQUENCE_CLAIMS_MODEL_HOSTS | none | Host names the address may use besides 127.0.0.1 and localhost, separated by commas. Such a host must use https:// |
BIOSEQUENCE_CLAIMS_MODEL_KEY | none | A key the service sends to the model as Authorization: Bearer, if the model needs one. It is never written to the log or to a case |
BIOSEQUENCE_CLAIMS_MODEL_SECONDS | 900 with openai-chat, 120 otherwise | How long the service waits for the model, 5 to 3,600 seconds. A model on processors alone can take minutes for one case |
BIOSEQUENCE_PAPERS | none | A paper folder made with the papers command, such as /data/papers/<id>. With it, each paper a hit cites reads as its PubMed citation, dated to the day it first came out, with its marks. See Paper citations and marks |
BIOSEQUENCE_ROR | none | A ROR name index made with the ror command, such as /data/ror/v2.15-2026-10-20. With it, the Applicants tab suggests the ROR record of an applicant whose name matches. See ROR suggestions |
BIOSEQUENCE_ADDONS | none | The add-ons to run, separated by commas: clinical for clinical context and funding for the funding trail. Empty or none runs none. An add-on name this version does not know stops the service at start. See Clinical context and Funding trail |
BIOSEQUENCE_CLINICAL | none | The clinical context folder that load made, such as /data/clinical/<release>. The service does not start when BIOSEQUENCE_ADDONS lists clinical and this is unset, or when this is set and clinical is not listed |
BIOSEQUENCE_FUNDING | none | The funding trail folder that load made, such as /data/funding/<release>. The service does not start when BIOSEQUENCE_ADDONS lists funding and this is unset, or when this is set and funding is not listed |
NODE_EXTRA_CA_CERTS | none | A PEM file of your own certificate authority, if your provider's certificate comes from one |
With sign-in on, the service needs at least one rule for who may use it: BIOSEQUENCE_INVITE_FILE, BIOSEQUENCE_ALLOWED_DOMAINS or BIOSEQUENCE_OIDC_REQUIRED_GROUP. Write the settings says how the rules combine. A setting that is missing or does not fit stops the service at start with a message that names it.
Commands
Run a command as docker run --rm -v sequences-data:/data conductscience/sequences:<version> <command>. check-license also needs -v /etc/sequences:/config:ro, and load the import folder, as shown above. Only serve and check-license read the license file. A snapshot's ID is the name of its folder under corpora/, and this lists them:
docker run --rm -v sequences-data:/data --entrypoint ls conductscience/sequences:<version> /data/corpora
| Command | What it does |
|---|---|
serve | Start the service (the default command) |
versions | Show the release and the version of every search program |
check-license | Show what the license file allows |
healthcheck | Exit with 0 if the service in the same container answers on 127.0.0.1. The image's health check runs it inside the running container, and Health and logs shows how to read the result |
load <folder> --sums <file> | Load a snapshot, delta, clinical context or funding trail copy, checking every file |
verify <snapshotId> [--delta <deltaId>] | Run a snapshot's or delta's checks again |
sums <snapshotId> [--delta <deltaId>] | Print the SHA-256 list of a snapshot or delta folder. sums --folder <folder> prints any folder's, such as a clinical context or funding trail folder |
attribution <snapshotId> [--delta <deltaId>] | Print the sources and terms of a snapshot or delta |
fetch, build, delta, deltas | Download the public source files, build a snapshot, add newer files as a delta, and list deltas |
papers --out <folder> --snapshot <folder> --pubmed <folder> ... | Make a paper folder from PubMed's files and the Retraction Watch Database. See Paper citations and marks |
ror --zip <file> --out <folder> | Make the name index for ROR suggestions from ROR's data zip. See ROR suggestions |
sample add <search ID> --name <name> --title <words> --source <words> --source-url <https link> | Make a sample result from a finished search. See Sample results |
sample list, sample remove <name> | List the sample results, or remove one |
audit-export --day <YYYY-MM-DD>, or --from <time> --to <time> | Write the audit log and the case logs for a UTC day or a time range as JSON lines for a SIEM, reading the data volume only. See Audit export for a SIEM |
help | List every command and its options |
Health and logs
The image checks itself every 30 seconds, after a 90-second start period, by asking the service on 127.0.0.1 inside the container. To see the result, run:
docker inspect --format '{{.State.Health.Status}}' sequences
healthy means the service answers. At start the service may wait up to 50 seconds for the lock (.lock) that a crashed container left on the data volume to expire. See Upgrades.
For an uptime monitor outside the container, read https://sequences.example.org/api/health with sign-in on, or http://127.0.0.1:8765/api/health on the machine itself. It needs no sign-in. It answers 200 with {"ok":true} when BLAST+ answers and a snapshot is installed, and 503 with {"ok":false} otherwise. It says nothing else, and it asks the search tools at most every 30 seconds however often it is read. With sign-in on, it also answers at the server's IP address, for a load balancer's health check, which cannot name your host. Nothing else answers there.
The service writes its log to standard output and standard error, so docker logs sequences shows it:
- At start: the host names and issuer (sign-in on) or the local address (sign-in off), the license summary, the data folder, the paper folder if you set one, whether watch emails are on, the ROR release that applicant suggestions come from, or that they are off, and the add-ons that run (
Add-ons: none.when none do). - Sign-in problems, such as a provider that cannot be reached, a token that fails a check, or an account that is refused, with the reason.
- A license file or invite list that cannot be read.
- Each API token made or revoked, with the account and the token's name, never the token itself.
Sign-ins, API tokens, starts and invite file changes also go into the service's audit log on the data volume, which keeps them. See Audit export for a SIEM.
Queries, sequences and results are never written to the log. Docker keeps logs without a size limit unless you set one. These options to docker run keep five files of 10 MB:
--log-opt max-size=10m --log-opt max-file=5
Usage
With sign-in on, the people named in BIOSEQUENCE_ADMINS see a Usage link beside their account. The usage page, /admin, counts searches and other work (alignments, imports and portfolio exports) for each account and day over the last 7, 30 or 90 days. A day is the UTC date a job started. The page names each account as the user claim named it at its last sign-in, which the service keeps in accounts.json on the data volume. For anyone else, the page does not exist. On a server that runs under a license, the page starts with the license's plan and last day, and with its message when the end is near or new work has stopped. See The license file.
The usage page also lists every API token: whose it is, its name, when it was made, last used and expires, and whether it is active, expired or revoked, and by whom. An admin can revoke any token there, and the person who made it then reads "Revoked by an admin". Searches a program starts with a token count for the token's account, as any search does.
An admin can also download a file of the decisions in every case on the server, to measure how well the claim rules and model prompts work: GET /api/admin/decisions.csv, or GET /api/admin/decisions.json, which adds totals. Each row is a claim feature, a call to the claims model, a dropped suggestion, or a decision on a drafted sentence. A row gives the case ID, the account by the first 8 characters of its ID (the ref that GET /api/admin/usage gives), the claims rules version, the feature's kind, rule, source and status and whether it was edited, the model and the prompt's ID and SHA-256, and a drop reason in fixed words. The file holds no claim words, quotes, notes or edits, since claims pasted into a case can be from an application that is not yet published. A case's own decisions, with its words, download from the case's Report step. See The decisions file.
Admins can also export the service's audit log with every case's log. See Audit export for a SIEM.
Audit export for a SIEM
With sign-in on, the service keeps its own audit log, and an admin can export it with every case's log as JSON lines for a SIEM, a UTC day or any time range at a time. Every event in these logs carries the SHA-256 hash of the event before it and its own hash, so a changed, missing or reordered event breaks the chain from that event on. The export carries the hashes, so you can check them.
The service's log is audit/events.jsonl on the data volume, with its last event named in audit/head.json. It holds:
- Each sign-in, refused sign-in, failed sign-in and sign-out: the time, the account as the user claim names it, its ID at your provider, and for a refused or failed one the reason in fixed words. A refused sign-in keeps at most 100 characters of the name your provider sent. A failed one names no account, since none was confirmed.
- A failed sign-in only when your provider's answer comes back to a sign-in this server started: the request to
/api/auth/callbackmust carry the state the server gave the browser at the start. Anyone can call that address, so a call without the state is not noted. At most 30 failed sign-ins are noted a minute; past that, onesignin.failed.suppressedevent gives the count of the rest and the minute, once the minute is over or when the service stops. - Each API token made or revoked, by the token's ID, with who revoked it. Never the token or its name.
- Each start: the release, the license's ID, plan, state and last day, each setting in Settings that is set, and the names of those that changed since the last start. A secret, a key, or a setting that names a secret's file shows as
set, and an address shows without its user name, query or fragment. - Each change to the invite file, when the service next reads it: the entries added and removed.
- Each export through the API, with the admin who made it and what it held.
It holds no IP addresses, queries, sequences or results. Searches, page views and the container's commands (such as load or sample add) are not in it; the usage page counts searches. On ConductScience's hosted service, a free trial is noted when it starts, and a trial ConductScience lengthens later is not. The service only adds to the log. Nothing rotates, rewrites or deletes it, and it grows by about 300 bytes a sign-in. With sign-in off there is no log.
To export a day, with an admin's API token (see API tokens for programs):
curl -H "Authorization: Bearer <token>" -o sequences-audit-2026-11-12.jsonl \
"https://sequences.example.org/api/admin/audit.jsonl?day=2026-11-12"
dayis a UTC date. For another range, givefromandtoinstead, as times with a time zone, such asfrom=2026-11-12T00:00:00Z&to=2026-11-12T12:00:00Z.fromis in the range andtois not.- Only the people in
BIOSEQUENCE_ADMINScan export. For anyone else, and with sign-in off, the address does not exist. - The container's
audit-exportcommand writes the same lines from the data volume, which it only reads, so it can run beside the service, from a scheduled job on the server. The service's log does not note these exports. The command exits with 1 after writing when a log's chain is broken:
docker run --rm -v sequences-data:/data:ro conductscience/sequences:<version> audit-export --day 2026-11-12 \
> sequences-audit-2026-11-12.jsonl
Each line is one JSON object, and its record says what it is:
event: one event.lognames its log:service,case/<case ID>, orjoins/<folder>for an account's applicant joins. For the service's log,eventis the event as stored. For a case or joins log it keeps the event's number, time, user, type, IDs, counts and hashes, andwithheldnames the fields left in the log. Case names, claim text, notes, reasons, the words of summaries and other text stay out, since claims pasted into a case can be from an application that is not yet published.chain, after each log's events:firstandlast, the numbers of the events given;prev, the hash of the event beforefirst, or null at the log's start;head, the last event the log's record names; andcheck, the service's check of the whole log as it exported it, withokfalse and the first broken event when it fails.export, last: the range, when the file was made, and the numbers of events, logs and broken logs, with the logs it could not read inskipped.
A log's events in a file run from its first event in the range to its last, so they always follow one another in its chain, and the next day's file goes on from there.
To check exports, save the script below as check_audit.py. It needs Python 3 and nothing else. Give it the exports oldest first. It checks that each log's events follow one another and that each event's prev is the hash of the one before, across files too, and it recomputes the hash of each event given whole. With --data and the data volume, it also reads each log whole, recomputes every hash, checks each log's head, and checks each exported event against the log. The image has Python 3, so the second check can run there with the volume read-only:
python3 check_audit.py sequences-audit-2026-11-12.jsonl sequences-audit-2026-11-13.jsonl
docker run --rm -v sequences-data:/data:ro -v "$PWD":/check:ro --entrypoint python3 \
conductscience/sequences:<version> /check/check_audit.py /check/sequences-audit-2026-11-12.jsonl --data /data
It prints each problem it finds and exits with 1, or ends with "the chains hold" and exits with 0. The script:
# check_audit.py checks Sequences audit exports with Python 3 and its standard library alone.
# python3 check_audit.py EXPORT [EXPORT ...] [--data FOLDER]
# Give exports oldest first. In each log it checks that the events follow one another, that each event's prev is the
# hash of the event before it, across exports too, and that each event exported whole still has its own hash. With
# --data and the data folder, it reads each log whole, recomputes every hash, checks the log's head, and checks that
# each exported event is the log's.
import argparse, decimal, hashlib, json, os, re, sys
SURROGATE = re.compile('[\ud800-\udfff]')
LOG = re.compile(r'(service|case/[0-9a-f-]{36}|joins/([0-9a-f]{64}|local))$')
NO_HEAD = {'seq': 0, 'hash': None}
def number(x):
# A float as JavaScript writes it, the shortest digits that read back the same.
if x == 0:
return '0'
t = decimal.Decimal(repr(abs(x))).as_tuple()
digits = ''.join(map(str, t.digits))
n = len(digits) + t.exponent
digits = digits.rstrip('0')
k, sign = len(digits), '-' if x < 0 else ''
if k <= n <= 21:
return sign + digits + '0' * (n - k)
if 0 < n <= 21:
return sign + digits[:n] + '.' + digits[n:]
if -6 < n <= 0:
return sign + '0.' + '0' * -n + digits
e = n - 1
return sign + digits[0] + ('.' + digits[1:] if k > 1 else '') + 'e' + ('+' if e >= 0 else '-') + str(abs(e))
def text(s):
return SURROGATE.sub(lambda m: '\\u%04x' % ord(m.group()), json.dumps(s, ensure_ascii=False))
def canonical(v):
# JSON as the service hashes it: as JavaScript writes it, with the keys of every object sorted.
if v is None or isinstance(v, bool):
return json.dumps(v)
if isinstance(v, int):
return str(v)
if isinstance(v, float):
return number(v)
if isinstance(v, str):
return text(v)
if isinstance(v, list):
return '[' + ','.join(map(canonical, v)) + ']'
keys = sorted(v, key=lambda key: key.encode('utf-16-be'))
return '{' + ','.join(text(key) + ':' + canonical(v[key]) for key in keys) + '}'
def sealed(event):
rest = {key: value for key, value in event.items() if key != 'hash'}
return hashlib.sha256(canonical(rest).encode('utf-8')).hexdigest() == event.get('hash')
def lines_of(path):
lines = []
with open(path, encoding='utf-8', newline='') as f:
for n, line in enumerate(f.read().split('\n'), 1):
if line.strip():
try:
value = json.loads(line)
except ValueError:
value = None
lines.append(value if isinstance(value, dict) else {'invalidLine': n})
return lines
def check_exports(exports, problems):
seen = {}
for n, lines in enumerate(exports, 1):
def say(log, seq, reason):
problems.append('export %d%s%s: %s' % (n, ', ' + log if log else '', ', event %s' % seq if seq else '', reason))
if any('invalidLine' in line for line in lines):
say(None, None, 'a line is not a JSON object')
tail = lines[-1] if lines else {}
if tail.get('record') != 'export' or tail.get('format') != 'sequences-audit':
say(None, None, 'it has no export line at its end, so it may be cut short')
logs = {}
for line in lines:
if line.get('record') in ('event', 'chain'):
logs.setdefault(line.get('log'), []).append(line)
for log, group in logs.items():
events = [line for line in group if line['record'] == 'event']
chain = next((line for line in group if line['record'] == 'chain'), None)
if chain is None:
say(log, None, 'it has no chain line')
continue
known = seen.setdefault(log, {})
first, prev, last = chain.get('first') or 0, chain.get('prev'), max(known, default=0)
if last and first > last + 1:
say(log, last + 1, 'events %d to %d are in none of the exports given' % (last + 1, first - 1))
if first - 1 in known and known[first - 1] != prev:
say(log, first, 'it does not follow the event an earlier export gave before it')
if len(events) != (chain.get('last') or 0) - first + 1:
say(log, None, 'its chain line and its events disagree on how many there are')
for seq, line in enumerate(events, first):
event = line.get('event')
if not isinstance(event, dict) or event.get('seq') != seq:
say(log, seq, 'this line is not the event with this number')
break
if event.get('prev') != prev:
say(log, seq, 'it does not follow the event before it')
if not line.get('withheld') and not sealed(event):
say(log, seq, 'it changed after it was written')
if known.get(seq, event.get('hash')) != event.get('hash'):
say(log, seq, 'an earlier export gave it another hash')
known[seq] = prev = event.get('hash')
check = chain.get('check') or {}
if check.get('ok') is False:
say(log, None, 'the server found the log broken when it exported it: %s' % (check.get('broken') or {}).get('reason'))
def read_log(data, log):
if not LOG.match(log or ''):
return None
kind, _, name = log.partition('/')
folder = os.path.join(data, {'service': 'audit', 'case': 'cases', 'joins': 'applicants'}[kind], name)
if not os.path.exists(os.path.join(folder, 'events.jsonl')):
return None
if kind == 'case':
with open(os.path.join(folder, 'case.json'), encoding='utf-8') as f:
head = json.load(f).get('head') or NO_HEAD
elif os.path.exists(os.path.join(folder, 'head.json')):
with open(os.path.join(folder, 'head.json'), encoding='utf-8') as f:
head = json.load(f)
else:
head = NO_HEAD
return lines_of(os.path.join(folder, 'events.jsonl')), head
def broken(events, head):
prev = None
for seq, event in enumerate(events, 1):
if event.get('seq') != seq:
return seq, 'this line is not the event with this number'
if event.get('prev') != prev:
return seq, 'it does not follow the event before it'
if not sealed(event):
return seq, 'it changed after it was written'
prev = event['hash']
if (head.get('seq'), head.get('hash')) != (len(events), prev):
return min(head.get('seq') or 0, len(events)) + 1, 'the log\'s head does not name its last event'
return None
def check_data(exports, data, problems):
logs = {}
for lines in exports:
for line in lines:
if line.get('record') == 'event':
logs.setdefault(line.get('log'), []).append(line)
for log, group in logs.items():
found = read_log(data, log)
if found is None:
problems.append('%s: the data folder has no such log' % log)
continue
events, head = found
fault = broken(events, head)
if fault:
problems.append('%s, event %d: %s' % (log, fault[0], fault[1]))
for line in group:
event = line.get('event') or {}
seq, withheld = event.get('seq'), line.get('withheld') or []
stored = events[seq - 1] if isinstance(seq, int) and 0 < seq <= len(events) else {}
if not event or stored.get('hash') != event.get('hash'):
problems.append('%s, event %s: the log does not hold it' % (log, seq))
continue
for key in set(stored) | set(event) | set(withheld):
if (key in event) == (key in withheld) or key not in stored or (key in event and canonical(event[key]) != canonical(stored[key])):
problems.append('%s, event %d: its %s does not match the log' % (log, seq, key))
def main():
parser = argparse.ArgumentParser(description='Check Sequences audit exports.')
parser.add_argument('exports', nargs='+', help='the export files, oldest first')
parser.add_argument('--data', help='the data folder, to check the logs themselves')
args = parser.parse_args()
exports = [lines_of(path) for path in args.exports]
problems = []
check_exports(exports, problems)
if args.data:
check_data(exports, args.data, problems)
for problem in problems:
print(problem)
events = sum(line.get('record') == 'event' for lines in exports for line in lines)
print('%d exports, %d events: %s' % (len(exports), events, '%d problems' % len(problems) if problems else 'the chains hold' + (', and every event matches the data folder' if args.data else '')))
return 1 if problems else 0
if __name__ == '__main__':
sys.exit(main())
Sample results
A sample result is a finished search saved as a read-only page, so the people you invite can see what a search finds before they sign in. While there is one, the sign-in page links to the samples at /samples. Anyone who can reach the service can read a sample without signing in, so make samples only from searches of public sequences.
Make one from a finished search of a patent set, named by its ID (the job in the search page's address) or its first 8 or more characters:
docker run --rm -v sequences-data:/data conductscience/sequences:<version> sample add <search ID> --name <name> \
--title "<what it shows>" --source "<the public record>" --source-url <https link to the record>
The name, up to 40 lowercase letters, digits and hyphens, gives the page its address, /samples/<name>. The command copies the search's hits, alignments and records into samples/ on the data volume, so a sample stays when its snapshot is replaced. It copies nothing about who ran the search and no marks. It refuses a search that belongs to a case, a batch or a combined search, an antibody regions search, and text that looks like an email address. sample list lists the samples, and sample remove <name> removes one. The service shows each change without a restart.
Watch emails
People can watch a finished search of the snapshot, or an applicant. When you load a delta, the service runs each watch on a search again within the hour and lists the matches it has not found before under Your watches on the search page. A watch on an applicant runs no search: within the hour it lists the applicant's publications that the delta adds.
With sign-in on, the service can also email what a watch finds. It sends email only through Resend, an email delivery service, so you need a Resend account, a sender address on a domain you have verified there, and both BIOSEQUENCE_MAIL_FROM and BIOSEQUENCE_RESEND_KEY. With one setting alone the service does not start, and the log names the missing one. With neither, no email goes out.
- Each email goes to the address in the person's email claim, so emails need
BIOSEQUENCE_OIDC_USER_CLAIM=email, the default. With another user claim, such aspreferred_username, watches show what they find on the page only. - Each email has a link that pauses the watch without signing in.
- With sign-in off there is no address to send to, so the service sends no email.
Claim suggestions from a model
A case's Review step can ask a language model for claim features the rules missed: SEQ ID references, identity thresholds, fragment lengths, residue ranges and written-out sequences. Suggestions are off until you name a model. Each one names the model and the exact characters it came from, and counts only once someone accepts it. The button that asks reads "(Pilot)".
Run the model on a server you control, behind a server that takes chat completions calls, such as vLLM, llama.cpp's server or Ollama, and set:
BIOSEQUENCE_CLAIMS_MODEL_URL=http://127.0.0.1:8000/v1/chat/completions
BIOSEQUENCE_CLAIMS_MODEL_API=openai-chat
BIOSEQUENCE_CLAIMS_MODEL=your-model-name
The service uses the address as written, so give the whole path. With sign-in on, 127.0.0.1 is the container itself: start the model's container with --network container:sequences, which shares that address, or name the model's host in BIOSEQUENCE_CLAIMS_MODEL_HOSTS and serve it over HTTPS. At start the log names the model, where it runs, the format and how long the service waits for it, and never the key.
The model receives the case's claims, the features the rules found in each, and, when the claims came from a publication's XML, up to 40 paragraphs of its description, 60,000 characters at most, that cite a SEQ ID NO the claims name or define a term the claims use. It is asked for JSON. A suggestion stays only when its quote is in its claim. Its details stay only when each number in them is written in that quote, and a quote from the description stays only when it is in the paragraph it names. When one fails, the suggestion stays without it, and the reason is recorded.
A model that runs on processors alone can take minutes for one case, so with openai-chat the service waits up to 900 seconds unless BIOSEQUENCE_CLAIMS_MODEL_SECONDS says otherwise. The call runs in the background: the page shows how long it has been asking, and people keep working on the case. The service makes one call at a time. While one runs, a request from any other case is refused with a message to ask again in a few minutes.
Each call is recorded in its case, whether it brings suggestions or fails: the request as sent and its SHA-256, the model's answer up to 1,000,000 characters, how long the call took, and each suggestion kept or dropped with the reason. The case's log holds the same record. A key, if you set one, goes only in the Authorization header and is never recorded.
Paper citations and marks
A paper folder gives each paper a hit cites its PubMed citation and the day it first came out, online or in print, so Published before compares days rather than years. It also marks papers that were retracted, drew an expression of concern or were reinstated, from PubMed and the Retraction Watch Database. With PubTator 3 in it, a case can draft its paper sentence: the genes and diseases a paper names. The papers command makes the folder from files you download, with no network connection, and the service only reads it.
Download these into /srv/papers on the server. PubMed's two folders held about 75 GB in November 2026, and NLM's terms are in the README.txt of each.
| Path | What to download |
|---|---|
/srv/papers/baseline/ | PubMed's baseline files, pubmed*.xml.gz, from https://ftp.ncbi.nlm.nih.gov/pubmed/baseline/ |
/srv/papers/updatefiles/ | PubMed's update files, pubmed*.xml.gz, from https://ftp.ncbi.nlm.nih.gov/pubmed/updatefiles/ |
/srv/papers/deleted.pmids.gz | NLM's list of deleted PubMed IDs, from https://ftp.ncbi.nlm.nih.gov/pubmed/deleted.pmids.gz |
/srv/papers/retraction_watch.csv | The file of that name from Crossref's Retraction Watch data repository, https://gitlab.com/crossref/retraction-watch-data. Note the commit you took it from and that commit's date |
/srv/papers/bioconcepts2pubtator3.gz | If you like, about 6 GB: PubTator 3's genes and diseases, from https://ftp.ncbi.nlm.nih.gov/pub/lu/PubTator3/. Note the date the folder lists for the file |
Then make the folder. --out is the parent folder: the command makes a folder named by its ID inside it, and prints the ID at the end. Give --snapshot once for each snapshot you search. A snapshot's ID is the name of its folder under corpora/. Give the Retraction Watch commit and its date, and PubTator's date, as you noted them. If you skipped PubTator 3, leave out --pubtator and --pubtator-date. Without the Retraction Watch file, leave out its three options, and the marks come from PubMed alone. The command uses one thread for each CPU, up to 12, unless you give --threads. Allow the container 12 GB of memory:
docker run --rm --memory 12g -e NODE_OPTIONS=--max-old-space-size=6144 \
-v sequences-data:/data -v /srv/papers:/in:ro conductscience/sequences:<version> \
papers --out /data/papers --snapshot /data/corpora/<snapshotId> \
--pubmed /in/baseline --pubmed /in/updatefiles --deleted /in/deleted.pmids.gz \
--retraction-watch /in/retraction_watch.csv --retraction-watch-commit <commit> --retraction-watch-date <YYYY-MM-DD> \
--pubtator /in/bioconcepts2pubtator3.gz --pubtator-date <YYYY-MM-DD>
Set BIOSEQUENCE_PAPERS=/data/papers/<id>, with the ID the command printed, then remove the container and start it again. The log then says Papers come from paper folder <id>: ..., and Sources and terms credits PubMed, and the Retraction Watch Database and PubTator 3 when the folder holds them. If the folder is not a finished paper folder, the service does not start, and the log says why.
A search keeps the paper entries it had when it ran, so a search from before the folder shows no marks until it runs again. For newer PubMed files or a new snapshot, make a new folder and point the setting at it. Without the setting, each paper keeps the year its snapshot gives it.
ROR suggestions
The Applicants tab can suggest the record in the Research Organization Registry (ROR) of an applicant whose name matches an organization's name there. A suggestion is only a link: it never joins or renames applicants. It needs a name index on your server, made once for each ROR release.
Download ROR's data file, a zip of about 39 MB under CC0, from https://doi.org/10.5281/zenodo.6347574 into a folder on the server, such as /srv/ror. Then make the index in the data volume. The command reads the zip and makes no network connection. Here --out names the index folder itself, not its parent. Name it after the JSON file in the zip, which unzip -l lists: v2.15-2026-10-20-ror-data.json gives v2.15-2026-10-20.
docker run --rm -v sequences-data:/data -v /srv/ror:/in:ro conductscience/sequences:<version> \
ror --zip /in/<ROR zip> --out /data/ror/v2.15-2026-10-20
Set BIOSEQUENCE_ROR=/data/ror/v2.15-2026-10-20, then remove the container and start it again. The log then says Applicant suggestions from ROR v2.15, data of 2026-10-20., and Sources and terms credits ROR. If the folder is not a finished index, the service does not start, and the log says why. For a newer ROR release, make a new folder and point the setting at it. Without the setting, the Applicants tab works and suggests nothing.
Clinical context
Clinical context is an add-on: it runs only when BIOSEQUENCE_ADDONS names it. Under a protein hit whose UniProt entry is a drug target, it shows the drugs that act on the target with their mechanisms, the diseases each is studied for with the highest clinical stage, and the trials, with their phase and whether they posted results. It shows on hits of the Swiss-Prot and TrEMBL sets. Patent, EST and attached database hits show none. Its data come from Open Targets (with ChEMBL), ClinicalTrials.gov and AACT's copy of trial results, in one folder that ConductScience builds for each release of them. Without the add-on, the service reads no such folder, and the page and the API show none of it.
The folder comes as a copy with its SHA-256 list, as a snapshot does, and holds tens of megabytes. To get it, write to support@conductscience.com. Put both in the import folder and make them readable, as in Load a copy, then load the copy into the data volume. load checks every file against the list, opens the folder as the service will and makes it read-only at /data/clinical/<release>, under the release name its folder carries. It makes no network connection.
docker run --rm \
-v sequences-data:/data \
-v /srv/sequences/import:/import:ro \
conductscience/sequences:<version> \
load /import/<release> --sums /import/<release>.SHA256SUMS
Set BIOSEQUENCE_ADDONS=clinical and BIOSEQUENCE_CLINICAL=/data/clinical/<release>, then remove the container and start it again. The log then says Clinical context from <release>: ... with the releases of its data, and Sources and terms credits Open Targets, ChEMBL, ClinicalTrials.gov and AACT. If the folder is missing or fails its checks, the service does not start, and the log says why. For a newer release of the data, load its folder and point BIOSEQUENCE_CLINICAL at it. To stop showing clinical context, remove both settings, then remove the container and start it again.
Funding trail
The funding trail is an add-on: it runs only when BIOSEQUENCE_ADDONS lists funding. It lists the grants PubMed gives for each paper under a hit. Grants that match an NIH project show as NIH awards, with the project number linked to NIH RePORTER, the title, the institutions and fiscal years, and the investigators. The other grants follow as PubMed lists them. It shows no award amounts. ConductScience builds its data, PubMed's grant lists and NIH RePORTER's awards, into one folder, and each new build is a new release of the data. Without the add-on, the service reads no such folder, and the page and the API show none of it.
A folder can also hold the statements on federally sponsored research in US published applications with a sequence listing, word for word. Where it does, up to three of them follow a hit's papers, under a line that names them, from the earliest applications that hold the hit's sequence, each in a part named by its application. The hit row in the API carries them as statements, and, when more of its applications give one, the number of the rest as statementsMore. A statement is not matched to an NIH award or to a paper's grants. A folder without statements works as before. Each week, a new release of the folder adds that week's applications to those of the weeks before. Upgrade the image first, as in Upgrades: an older image loads a folder with statements but shows none of them, and its log line leaves out their count.
The folder comes as a copy, as a snapshot does: the folder itself and its <release>.SHA256SUMS file. It holds tens of megabytes. To get it, write to support@conductscience.com. Put the folder and its SHA-256 list in the import folder and make them readable, as in Load a copy, then load the copy into the data volume. load checks every file against the list, opens the folder as the service will, and stores it read-only at /data/funding/<release>, where <release> is the release name the folder was built under. It makes no network connection.
docker run --rm \
-v sequences-data:/data \
-v /srv/sequences/import:/import:ro \
conductscience/sequences:<version> \
load /import/<release> --sums /import/<release>.SHA256SUMS
Add funding to BIOSEQUENCE_ADDONS and set BIOSEQUENCE_FUNDING=/data/funding/<release>, then remove the container and start it again. A server that also runs clinical context needs BIOSEQUENCE_ADDONS=clinical,funding. The log then says Funding trail from <release>: ... with the dates of its data, and with statements it ends with how many applications they cover and from which weeks. Sources and terms credits PubMed and NIH RePORTER, and the US published applications when the folder holds their statements. If the folder is missing or fails its checks, the service does not start, and the log says why. For a newer release of the data, load its folder, point BIOSEQUENCE_FUNDING at it, then remove the container and start it again. The old folder can stay, and nothing reads it. To stop showing the funding trail, take funding out of BIOSEQUENCE_ADDONS and remove BIOSEQUENCE_FUNDING, then remove the container and start it again.
Upgrades
The data volume and /etc/sequences stay as they are. Only the image changes. Back up first (see Backups), and upgrade when no searches are running, because stopping the service cancels them:
docker load -i sequences-<new version>.tar
docker run --rm conductscience/sequences:<new version> versions
docker stop sequences
docker rm sequences
Then start it with the same docker run command as before and the new version. Keep the previous image until you have checked the new one. The release notes list what each release changed.
Searches that docker stop cancels show as cancelled, and people can start them again. Searches a program sent in a batch are cancelled the same way, and the batch shows them as cancelled, so the program can send those again.
Only one service may use a data volume at a time, and it holds a lock (.lock) on the volume while it runs. A service that stops cleanly releases the lock at once. After a crash, the old container's lock expires 30 seconds after its last renewal. A new container waits for that, up to 50 seconds, then starts.
Backups
The data volume holds:
| Path in the volume | What it is | Back up? |
|---|---|---|
jobs/ | Every search and alignment, with its results, marks and record, each portfolio export's CSV and FASTA files, and each result file imported into a case, as it came, with its joined rows | Yes |
databases/ | FASTA collections imported on the page with sign-in off | Yes |
listings/, fragments.jsonl | Uploaded sequence listings and saved sequence fragments | Yes |
cases/ | Cases: their claims, search plans, searches added by hand or imported, notes and the log of every step. Reports are not kept, and the log has each one's SHA-256 | Yes |
watches/ | Watches: each one's search or applicant, the matches or publications it has seen and its runs, and the key behind the stop links in watch emails | Yes |
tokens.json, batches/ | API tokens (a hash of each, never the token itself, with its name, account and state) and each batch's list of searches | Yes |
applicants/ | Each account's applicant joins and the log of every join and undo | Yes |
audit/ | The service's audit log: sign-ins, API tokens, starts and their settings, invite file changes and audit exports. See Audit export for a SIEM | Yes |
papers/, ror/ | Paper folders and ROR name indexes, read-only | Yes. The service does not start without the folders its settings name, and a paper folder made again gets a new ID |
clinical/ | Clinical context folders, read-only, with clinical context on | Optional if you keep the copies you received: load makes the folders again from them |
funding/ | Funding trail folders, read-only, with the funding trail on | Optional if you keep the copies you received: load makes the folders again from them |
accounts.json | The name each account last signed in with, for the usage page | Yes |
sign-in-provider.json | The sign-in provider the volume belongs to. See Move to another provider | Yes |
session-secret, calibration.json | The cookie key and the run-time estimates | Yes |
corpora/, deltas/ | Snapshots and deltas, read-only | No, if you keep the copies you loaded: load them again from those. A snapshot you built yourself cannot be built again from the same files, since GenBank keeps only its current release, so back it up as below |
inputs/, work/, .lock | Source files for a build, scratch space, and the running service's claim | No |
Back up /etc/sequences too: the license, the settings and the client secret. On the server:
sudo install -d -m 0700 /srv/backup
sudo tar -czf /srv/backup/sequences-config-$(date +%F).tar.gz -C /etc sequences
The command below copies the data volume, all but the snapshots, deltas and scratch files. To keep snapshots and deltas you built yourself, leave out --exclude=./corpora --exclude=./deltas. Stop the service first with docker stop sequences, which cancels any search still running, and start it again afterwards with docker start sequences. The one-off container runs tar as root so it can write to your backup folder. The archive holds the cookie key, so keep it as safe as the server:
docker run --rm --user 0 \
-v sequences-data:/data:ro \
-v /srv/backup:/backup \
--entrypoint tar conductscience/sequences:<version> \
--exclude=./corpora --exclude=./deltas --exclude=./inputs --exclude=./work --exclude=./.lock \
-czf /backup/sequences-data-$(date +%F).tar.gz -C /data .
To restore on a new server, or after you remove the old volume (docker volume rm sequences-data), put the settings back, create the volume and unpack the archive. Then load the same snapshots and deltas again, unless the archive holds them, so older results can open their records:
sudo tar -xzpf /srv/backup/sequences-config-<date>.tar.gz -C /etc
docker volume create sequences-data
docker run --rm --user 0 \
-v sequences-data:/data \
-v /srv/backup:/backup:ro \
--entrypoint tar conductscience/sequences:<version> \
-xzpf /backup/sequences-data-<date>.tar.gz -C /data
Network traffic
What the container receives:
- Sign-in off: connections to port 8765 on the server's own
127.0.0.1, through host networking. - Sign-in on: connections from your reverse proxy, on the same server, to
127.0.0.1:8765. The service answers only requests that name an allowed host, andGET /api/healthat an IP address. With the AWS kit, its load balancer connects to the server's private address instead. See In your AWS account (beta). - The service keeps an idle connection open for 65 seconds, and no setting changes that. A proxy or load balancer that reuses connections should close idle ones sooner, as an AWS load balancer does after 60 seconds.
What the container sends while it runs:
- Sign-in off: nothing, unless you set a claims model (below). With sign-in off the service sends no watch email.
- Sign-in on: requests to your sign-in provider, over HTTPS. The discovery document and the provider's public keys, each kept for an hour and fetched again early when a token names a key the service has not seen; the token endpoint, once per sign-in; and the userinfo endpoint, only when the ID token leaves out a claim the service needs.
- The search programs run on the snapshot in the data volume. BLAST+ can send usage reports to NCBI, and the service turns that off (
BLAST_USAGE_REPORT=false) for every program it starts. - Claim suggestions from a language model are off unless you set
BIOSEQUENCE_CLAIMS_MODEL_URLandBIOSEQUENCE_CLAIMS_MODEL. With them set, a case's claims, with the paragraphs of its description chosen for them, go to that address when someone asks for suggestions. The address may usehttp://only on127.0.0.1orlocalhost. Any other host must be named inBIOSEQUENCE_CLAIMS_MODEL_HOSTSand usehttps://. With sign-in on,127.0.0.1is the container itself, so a model on the same server needs a container that shares that address (--network container:sequences) or a named host with HTTPS. See Claim suggestions from a model. - Watch emails are off unless you set
BIOSEQUENCE_MAIL_FROMandBIOSEQUENCE_RESEND_KEY. With them set, the service posts each email tohttps://api.resend.com/emailsover HTTPS: the person's address, the watch's name and its new matches or publications. - Clinical context and the funding trail, when they run, read only their folders in the data volume and make no network connections.
- The license is checked inside the container against the key built into the image. There is no license server, and the service sends no usage data anywhere.
What the browser loads: the page, its scripts, styles and font, all from the service itself. Links leave it only when someone clicks them: the patent link on each result and on each new publication a watch lists (BIOSEQUENCE_PATENT_LINK), the PubMed and DOI links of the papers a hit cites and of their notices, the terms pages under Sources and terms, with BIOSEQUENCE_ROR set, the link to a suggested ROR record (https://ror.org/<id>), and, with clinical context on, the links to Open Targets (https://platform.opentargets.org), ChEMBL (https://www.ebi.ac.uk/chembl) and ClinicalTrials.gov (https://clinicaltrials.gov). With the funding trail on, the page also links to NIH RePORTER (https://reporter.nih.gov).
Commands you run yourself: fetch downloads the public source files from NCBI, EBI and UniProt over HTTPS. build, delta, load, verify, sums, attribution, papers, ror and check-license make no network connections. To hold the service to this, allow its container outbound HTTPS to your provider's host names, to your claims model's host if you set one, to api.resend.com if you turn on watch emails, and nothing else. A container that runs fetch also needs outbound HTTPS to ftp.ncbi.nlm.nih.gov, ftp.ebi.ac.uk and ftp.uniprot.org.
Questions about installing Sequences go to support@conductscience.com.
