Custom databases¶
gapit db build turns any FASTA of reference genes into a fully built gapit-native database in one command: records.jsonl, the sequences projection, the BLAST index, the manifest. This page is a worked tour with real session output. For providers, db fetch, and the datadir layout, see Databases.
When you need this¶
- You screen for genes no provider ships: a private marker panel, lab-curated variants.
- You want a small database with only the genes you report on, so outputs stay short.
- You have an abricate-era
~~~FASTA and want it as a native gapit database.
Command reference¶
gapit db build NAME FASTA [OPTIONS]
| Option | Value | Meaning |
|---|---|---|
NAME (argument) |
text | Target database name (created under the datadir). |
FASTA (argument) |
path | Input FASTA: plain, abricate ~~~, or gapit\| headers, detected per record (.gz/.bz2 accepted). |
--tsv |
path | Metadata TSV: header row with gene/accession/function columns. |
--dbtype |
nucl or prot |
Force the molecule type (default: auto-detect from the sequences). |
--kind / -k |
gene or cluster |
Force the database kind (default: auto-detect from the input suffix; a disagreement is a usage error). |
--typing / -T |
path | gapit.typing/1 or /2 scoring spec: validated, copied into the db as typing.json, recorded in its manifest. |
--floors / -F |
path | gapit.floors/1 per-gene identity floors for reads-mode presence: validated, copied into the db as floors.json (gene builds only). |
--datadir |
path | Database directory (default: $GAPIT_DATADIR, then ~/.local/share/gapit/db). |
--description |
text | Default product for records whose FASTA header has no description text. |
--force |
flag | Overwrite the database if it already exists. |
--quiet |
flag | Silence stderr diagnostics. |
Build progress lines go to stderr, the one-line JSON receipt to stdout. The receipt carries db, records, dbtype, and destination, the same fields as db fetch. Screen blocks below show stdout only; gapit screen prints its Processing: lines to stderr (see Screening).
Every example below was executed in one session inside a scratch directory, so receipts quote its absolute path. To reproduce, run from the repo root:
export PATH="$PWD/.pixi/envs/default/bin:$PATH"
mkdir -p /tmp/opencode/customdb-docs && cd /tmp/opencode/customdb-docs
The input genes are synthetic, generated with Python's random module (a fixed seed keeps this page reproducible; never use real sequences you can't share):
import random
rng = random.Random(11)
g1 = "".join(rng.choice("ACGT") for _ in range(240))
rng = random.Random(12)
g2 = "".join(rng.choice("ACGT") for _ in range(240))
open("genes.fa", "w").write(
f">labcur1 synthetic tetracycline efflux pump\n{g1}\n"
f">labcur2 synthetic macrolide esterase\n{g2}\n"
)
$ grep '>' genes.fa
>labcur1 synthetic tetracycline efflux pump
>labcur2 synthetic macrolide esterase
Worked examples¶
1. Plain FASTA to first hit¶
Build a database named labgenes into a local datadir, then screen a query contig that carries the first 200 bases of labcur1 plus flanking bases:
$ gapit db build labgenes genes.fa --datadir ./db
gapit: generated /tmp/opencode/customdb-docs/db/labgenes/sequences
gapit: self-check passed for labgenes
gapit: BLAST index built (nucl)
{"db":"labgenes","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labgenes"}
$ gapit screen query1.fa --db labgenes --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query1.fa contigA 31 230 + labcur1 1-200/240 =============.. 0/0 83.33 100.00 labgenes synthetic tetracycline efflux pump
The hit row decodes the plain header: gene labcur1, product from the header description, empty ACCESSION and RESISTANCE because a plain header carries neither. %COVERAGE is 83.33 because the query holds 200 of the gene's 240 bases; the trailing .. in the coverage map sketches those 40 uncovered bases.
2. Attaching metadata with --tsv¶
A metadata TSV adds accession and function classes per gene. Header row mandatory, gene column mandatory, accession and function optional per file:
$ cat meta1.tsv
gene accession function
labcur1 LAB-0001 tetracycline
labcur2 LAB-0002 macrolide
$ gapit db build labmeta genes.fa --datadir ./db --tsv meta1.tsv
gapit: generated /tmp/opencode/customdb-docs/db/labmeta/sequences
gapit: self-check passed for labmeta
gapit: BLAST index built (nucl)
{"db":"labmeta","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labmeta"}
$ gapit screen query1.fa --db labmeta --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query1.fa contigA 31 230 + labcur1 1-200/240 =============.. 0/0 83.33 100.00 labmeta LAB-0001 synthetic tetracycline efflux pump tetracycline
The function column is ;-separated when a gene belongs to several classes. Both classes land in the RESISTANCE column as one string:
$ cat meta2.tsv
gene accession function
labcur1 LAB-0001 virulence;marker
labcur2 LAB-0002 macrolide
$ gapit db build labmulti genes.fa --datadir ./db --tsv meta2.tsv
gapit: generated /tmp/opencode/customdb-docs/db/labmulti/sequences
gapit: self-check passed for labmulti
gapit: BLAST index built (nucl)
{"db":"labmulti","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labmulti"}
$ gapit screen query1.fa --db labmulti --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query1.fa contigA 31 230 + labcur1 1-200/240 =============.. 0/0 83.33 100.00 labmulti LAB-0001 synthetic tetracycline efflux pump virulence;marker
3. abricate-style ~~~ headers¶
Files exported from abricate conventions build without changes. Fields split on ~~~, and the description after the last field becomes the product:
$ grep '>' legacy.fa
>oldlab~~~tetA_lab~~~SYN-100~~~TETRACYCLINE synthetic tetracycline pump
>oldlab~~~ermX~~~SYN-101~~~MACROLIDE synthetic methylase
$ gapit db build oldlab legacy.fa --datadir ./db
gapit: generated /tmp/opencode/customdb-docs/db/oldlab/sequences
gapit: self-check passed for oldlab
gapit: BLAST index built (nucl)
{"db":"oldlab","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/oldlab"}
$ gapit screen query_tet.fa --db oldlab --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query_tet.fa contigB 1 240 + tetA_lab 1-240/240 =============== 0/0 100.00 100.00 oldlab SYN-100 synthetic tetracycline pump TETRACYCLINE
The first ~~~ field is the original database name and is discarded: db is always the NAME you are building. Gene, accession, and resistance all flow through to the screen output.
4. Roundtripping through gapit| headers¶
Native records are portable. Regenerate a FASTA from the records.jsonl written by example 1 (one gapit| header per record, sequence and product verbatim), then rebuild it as a new database:
$ grep '>' ported.fa
>gapit|db=labgenes|gene=labcur1|acc=|func= synthetic tetracycline efflux pump
>gapit|db=labgenes|gene=labcur2|acc=|func= synthetic macrolide esterase
$ gapit db build labgenes_rt ported.fa --datadir ./db
gapit: generated /tmp/opencode/customdb-docs/db/labgenes_rt/sequences
gapit: self-check passed for labgenes_rt
gapit: BLAST index built (nucl)
{"db":"labgenes_rt","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labgenes_rt"}
$ gapit screen query1.fa --db labgenes_rt --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query1.fa contigA 31 230 + labcur1 1-200/240 =============.. 0/0 83.33 100.00 labgenes_rt synthetic tetracycline efflux pump
The screen output is identical to example 1 (the db value in the header is retargeted to the new NAME). Empty acc= and func= segments are fine, and a gapit| header carrying accession and function values roundtrips them the same way.
5. Protein databases (blastx)¶
Amino acid input is detected automatically. Note the receipt says "dbtype":"prot" and the BLAST index line says (prot):
$ grep '>' toxins.faa
>toxA synthetic pore-forming toxin
$ gapit db build toxins toxins.faa --datadir ./db
gapit: generated /tmp/opencode/customdb-docs/db/toxins/sequences
gapit: self-check passed for toxins
gapit: BLAST index built (prot)
{"db":"toxins","records":1,"dbtype":"prot","destination":"/tmp/opencode/customdb-docs/db/toxins"}
$ gapit db build toxins2 toxins.faa --datadir ./db --dbtype prot
gapit: generated /tmp/opencode/customdb-docs/db/toxins2/sequences
gapit: self-check passed for toxins2
gapit: BLAST index built (prot)
{"db":"toxins2","records":1,"dbtype":"prot","destination":"/tmp/opencode/customdb-docs/db/toxins2"}
Screening a protein database runs blastx, so the query file must be nucleotide. Here the query contig carries the toxin's coding sequence, back-translated one codon per amino acid, with flanking bases:
$ gapit screen query_toxin.fa --db toxins --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query_toxin.fa contigT 31 390 + toxA 1-120/120 =============== 0/0 100.00 100.00 toxins synthetic pore-forming toxin
Reading the row: START/END are nucleotide coordinates on the contig (30 bp flank, then the 360 bp CDS), while COVERAGE and %COVERAGE count amino acids over the 120-residue protein. --dbtype nucl|prot exists for the edge case where the letter alphabet alone would mislead the heuristic; for ordinary inputs the auto-detect and the explicit flag agree.
6. Compressed input¶
.gz (and .bz2) inputs build exactly like plain ones:
$ gapit db build labgz genes.fa.gz --datadir ./db
gapit: generated /tmp/opencode/customdb-docs/db/labgz/sequences
gapit: self-check passed for labgz
gapit: BLAST index built (nucl)
{"db":"labgz","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labgz"}
7. Iterating: rebuild with --force¶
Add a third gene to the FASTA and rebuild labgenes from example 1. An existing database is never overwritten silently:
$ grep '>' genes_v2.fa
>labcur1 synthetic tetracycline efflux pump
>labcur2 synthetic macrolide esterase
>labcur3 synthetic fosfomycin thiolase
$ gapit db build labgenes genes_v2.fa --datadir ./db
{"schema":"gapit.error/1","code":"DB_ALREADY_EXISTS","message":"won't overwrite existing database labgenes (use --force)","context":{"db":"labgenes"}}
$ echo $?
4
The envelope is the standard gapit.error/1 shape (exit 4, database error).
--force deletes and rebuilds in place. The metadata TSV from example 2 still applies:
$ gapit db build labgenes genes_v2.fa --datadir ./db --tsv meta1.tsv --force
gapit: generated /tmp/opencode/customdb-docs/db/labgenes/sequences
gapit: self-check passed for labgenes
gapit: BLAST index built (nucl)
{"db":"labgenes","records":3,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labgenes"}
$ gapit screen query1.fa --db labgenes --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query1.fa contigA 31 230 + labcur1 1-200/240 =============.. 0/0 83.33 100.00 labgenes LAB-0001 synthetic tetracycline efflux pump tetracycline
The receipt now reports records: 3. Edit records.jsonl or the FASTA, rebuild with --force, and every downstream artifact regenerates.
8. Metadata edge rules¶
Duplicate rows for one gene: the first row wins, a warning goes to stderr (--quiet silences it), and the build succeeds. The screen output proves which row survived:
$ cat dup.tsv
gene accession function
labcur1 KEEP-1 tetracycline
labcur1 LOST-2 macrolide
$ gapit db build labdup genes.fa --datadir ./db --tsv dup.tsv
WARNING: duplicate gene 'labcur1' in metadata TSV: keeping the first row
gapit: generated /tmp/opencode/customdb-docs/db/labdup/sequences
gapit: self-check passed for labdup
gapit: BLAST index built (nucl)
{"db":"labdup","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labdup"}
$ gapit screen query1.fa --db labdup --datadir ./db
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
query1.fa contigA 31 230 + labcur1 1-200/240 =============.. 0/0 83.33 100.00 labdup KEEP-1 synthetic tetracycline efflux pump tetracycline
A TSV row naming a gene the FASTA does not carry is warned about and skipped. The FASTA is the truth for what exists:
$ cat ghost.tsv
gene accession function
labcur1 LAB-0001 tetracycline
ghostgene G-999 virulence
$ gapit db build labghost genes.fa --datadir ./db --tsv ghost.tsv
WARNING: gene 'ghostgene' in metadata TSV not found in FASTA: skipped
gapit: generated /tmp/opencode/customdb-docs/db/labghost/sequences
gapit: self-check passed for labghost
gapit: BLAST index built (nucl)
{"db":"labghost","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labghost"}
$ grep -c '"gene"' db/labghost/records.jsonl
2
9. Where builds land without --datadir¶
The datadir resolves per call: --datadir, then $GAPIT_DATADIR, then ~/.local/share/gapit/db. This example points GAPIT_DATADIR at a scratch directory (unset, the same command writes into the default ~/.local/share/gapit/db):
$ export GAPIT_DATADIR=/tmp/opencode/customdb-default/db
$ gapit db build labenv genes.fa
gapit: generated /tmp/opencode/customdb-default/db/labenv/sequences
gapit: self-check passed for labenv
gapit: BLAST index built (nucl)
{"db":"labenv","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-default/db/labenv"}
$ gapit db outdated
NAME FETCHED_AT AGE_DAYS STATUS
labenv 2026-09-27T14:46:27Z 0.00 ok
Unlike screening, a build creates a missing datadir instead of failing, so a fresh machine bootstraps on the first build. The scratch datadir was deleted after this capture.
10. Finishing the pipeline: two samples, one matrix¶
Screen two contig files against labmeta from example 2, save the report tables, and fold them into a summary matrix with gapit summary:
$ gapit screen sample1.fa --db labmeta --datadir ./db > sample1.tsv
$ gapit screen sample2.fa --db labmeta --datadir ./db > sample2.tsv
$ cat sample1.tsv
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
sample1.fa SAM001 41 280 + labcur1 1-240/240 =============== 0/0 100.00 100.00 labmeta LAB-0001 synthetic tetracycline efflux pump tetracycline
$ cat sample2.tsv
#FILE SEQUENCE START END STRAND GENE COVERAGE COVERAGE_MAP GAPS %COVERAGE %IDENTITY DATABASE ACCESSION PRODUCT RESISTANCE
sample2.fa SAM002 16 255 + labcur2 1-240/240 =============== 0/0 100.00 100.00 labmeta LAB-0002 synthetic macrolide esterase macrolide
$ gapit summary sample1.tsv sample2.tsv
#FILE NUM_FOUND labcur1 labcur2
sample1.tsv 1 + -
sample2.tsv 1 - +
Rules and edge behavior¶
Header auto-detection¶
The header kind is detected per record, so one FASTA can mix all three formats:
| FASTA header | Gene | Accession | Function | Product |
|---|---|---|---|---|
>mixplain plain header gene |
mixplain |
none | none | plain header gene |
>oldlab~~~fromtilde~~~SYN-100~~~TETRACYCLINE tilde gene |
fromtilde |
SYN-100 |
TETRACYCLINE |
tilde gene |
>gapit\|db=elsewhere\|gene=fromtag\|acc=SYN-200\|func=ampicillin;gentamicin tagged gene |
fromtag |
SYN-200 |
ampicillin, gentamicin |
tagged gene |
>secondplain (bare) |
secondplain |
none | none | --description, else secondplain |
For a plain header, the text after the id is the product when present. When absent, --description TEXT fills in, and with neither the gene name becomes the product:
$ grep '>' bare.fa
>baregene
$ gapit db build bare_desc bare.fa --datadir ./db --description "lab-curated reference gene"
gapit: generated /tmp/opencode/customdb-docs/db/bare_desc/sequences
gapit: self-check passed for bare_desc
gapit: BLAST index built (nucl)
{"db":"bare_desc","records":1,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/bare_desc"}
$ grep -o '"product":"[^"]*"' db/bare_desc/records.jsonl
"product":"lab-curated reference gene"
$ gapit db build bare_plain bare.fa --datadir ./db --force
gapit: generated /tmp/opencode/customdb-docs/db/bare_plain/sequences
gapit: self-check passed for bare_plain
gapit: BLAST index built (nucl)
{"db":"bare_plain","records":1,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/bare_plain"}
$ grep -o '"product":"[^"]*"' db/bare_plain/records.jsonl
"product":"baregene"
A header that starts with gapit| but is not valid tagged syntax fails the build with exit 4:
$ gapit db build labbroken broken_tag.fa --datadir ./db
{"schema":"gapit.error/1","code":"HEADER_MALFORMED","message":"malformed gapit/v1 sequence header: segment_without_key","context":{"seqid":"gapit|nonsense","reason":"segment_without_key"}}
The mixed file from the table above builds each record by its own rules, and the generated sequences projection shows the retargeting (db is always the NAME built, the original elsewhere tag is dropped):
$ gapit db build labmix mixed.fa --datadir ./db 2>/dev/null
{"db":"labmix","records":4,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labmix"}
$ grep '>' db/labmix/sequences
>gapit|db=labmix|gene=mixplain|acc=|func= plain header gene
>gapit|db=labmix|gene=fromtilde|acc=SYN-100|func=TETRACYCLINE tilde gene
>gapit|db=labmix|gene=fromtag|acc=SYN-200|func=ampicillin%3Bgentamicin tagged gene
>gapit|db=labmix|gene=secondplain|acc=|func= secondplain
Records keep input order, so records.jsonl line N is the Nth FASTA record. Duplicate gene names are allowed; a FASTA carrying twingene twice builds a two-record database:
$ gapit db build labtwin dupname.fa --datadir ./db --force
gapit: generated /tmp/opencode/customdb-docs/db/labtwin/sequences
gapit: self-check passed for labtwin
gapit: BLAST index built (nucl)
{"db":"labtwin","records":2,"dbtype":"nucl","destination":"/tmp/opencode/customdb-docs/db/labtwin"}
$ grep -c '"gene":"twingene"' db/labtwin/records.jsonl
2
dbtype resolution¶
--dbtype wins when given; otherwise the abricate mol-type heuristic decides from the input sequences themselves (nucl unless the letters say protein). Protein databases get a .pin BLAST index, and screening goes through blastx with a nucleotide query (example 5).
What lands in the database directory¶
$ ls db/labmeta
gapit-manifest.json
records.jsonl
sequences
sequences.ndb
sequences.nhr
sequences.nin
sequences.njs
sequences.not
sequences.nsq
sequences.ntf
sequences.nto
records.jsonl is the editable truth and sequences its gapit/v1-header projection; the sequences.n* files are the BLAST index. Field-level detail for both file contracts is in Databases. Sequences are stored verbatim, no provider-style normalization: this is your curated truth, and a manifest with source_urls: ["local"] certifies the build.
Troubleshooting¶
| Symptom | Error (exit code) | Fix |
|---|---|---|
| Build refuses an existing database | DB_ALREADY_EXISTS (4) |
Rebuild with --force (example 7). |
| Metadata TSV rejected | METADATA_MALFORMED (5) |
The first row must be a header containing a gene column. |
| Build fails on an empty input | BUILD_INVALID (4) |
The FASTA parsed to zero records; check the file has > headers. |
| FASTA or TSV path missing | INPUT_NOT_FOUND (5) |
The envelope's context.file names the unreadable path. |
Build fails on a gapit\| header |
HEADER_MALFORMED (4) |
Fix the tagged header; plain and ~~~ headers can't use gapit\| syntax. |
| Screen against the custom db finds nothing | none, exit 0 | Empty result tables are normal output. Loosen --minid/--mincov or check the query matches the strand; see Screening. |
Error envelopes are documented in Outputs; the full screening flag set in Screening.