Self-hosting is hereRead more →
Security architectureLast reviewed 10 November 2026

How ArmoryHub protects your records

This is the single reference for how ArmoryHub's encryption, authentication and infrastructure work: what we store, what we can see, what we cannot protect against, and what we still need to fix. Every statement on this page is checked against the shipped code. Where there is a weakness, it is listed, not left out.

01

The short version

  • Your records, photos and documents are encrypted on your device with AES-256-GCM under a random 256-bit master key. We never receive that key in plaintext.

  • The master key is protected by your 6-digit PIN, or by an optional passphrase. We hold neither. We cannot reset either. If you lose them and every signed-in device, the data is gone.

  • By default a PIN-protected copy of your master key is stored on our servers so your other devices can sync. A 6-digit PIN has about 20 bits of entropy. Someone holding a copy of our database could brute-force that copy offline in roughly a minute on one GPU. Enabling the optional passphrase replaces that copy with one that cannot be brute-forced. It is optional because it also raises the risk of losing your data; see The passphrase for the trade-off.

  • What we can see: your email, subscription, how many records of each type you have and when they changed. What we cannot see: the records, photos and documents themselves.

  • The client is closed source and has not had a third-party audit, because one costs tens of thousands of dollars and ArmoryHub is a one-person business. The source is available to security researchers on request. The internal audit of September 2026 produced the list under Known limitations.

02

What zero-knowledge means here, and what it does not

"Zero-knowledge" is a marketing phrase, so here is the precise claim. Every record is serialised to JSON on your device and encrypted there. The server receives ciphertext, stores ciphertext, and returns ciphertext. No server-side code of ours decrypts, searches, indexes, reports on or analyses your collection. Sorting, filtering, reports and ballistics all run in the browser after decryption.

The claim has two limits. First, the server can still see that you have records and how many, because each record type lives in its own table. Second, the strength of the claim against someone who copies our database depends on how your master key is protected, which is the subject of the next three sections. With the passphrase enabled the claim holds against anyone. In default PIN-only mode it holds against anyone unwilling to spend a minute of GPU time, which is not the standard we want, and is why the passphrase exists.

03

Key hierarchy

There are three keys. Your PIN and passphrase never encrypt data directly. They each derive a wrapping key, and the wrapping key protects the one key that actually encrypts your records.

PIN key

  • PBKDF2-HMAC-SHA256
  • 310,000 iterations
  • random 32-byte salt
  • → AES-256 wrapping key

Derived on your device each time you unlock. Never stored.

wraps

Master key

  • random AES-256 key
  • generated on your device
  • one per account
  • never leaves the device unwrapped

Encrypts every record, photo and document.

encrypts

Your records

  • JSON per record
  • AES-256-GCM
  • 12-byte random nonce
  • 16-byte auth tag

Stored as opaque bytes. Fresh nonce every time a record is saved.

Passphrase key (optional)

  • PBKDF2-HMAC-SHA256
  • 600,000 iterations
  • own 32-byte salt
  • → AES-256 wrapping key

Derived only when a new device needs the master key.

wraps a second copy of

Master key

  • same key as above
  • stored on the server
  • in place of the PIN-wrapped copy

See the next section.

Record format
nonce (12 bytes) ‖ ciphertext ‖ GCM tag (16 bytes)
Wrapped key format
nonce (12 bytes) ‖ 32-byte key ciphertext ‖ GCM tag
Crypto library
Web Crypto API (SubtleCrypto) only. No custom cryptography, no third-party crypto libraries.
Randomness
crypto.getRandomValues for every salt, nonce and key.
Key derivation choice
PBKDF2 rather than Argon2id because it is the only password-based KDF in Web Crypto. Argon2 would require shipping a WebAssembly implementation, which we have not done.
04

Where the keys live

The table below is the heart of the design. Read the "On our servers" column carefully.

ItemOn your deviceOn our servers
PIN, passphraseNever stored. Typed, used to derive a key, discarded. (Exception: biometric replace mode, see On your device.)Never.
Master key, unwrappedIn memory while unlocked, and in a browser-session key store so a page reload does not ask for the PIN again. Cleared on lock, on sign-out, and when the browser session ends.Never.
Master key wrapped by PIN keyYes, in IndexedDB. This is what your PIN unlocks every day.Yes, by default, so a new device can fetch it. Removed from the server when you enable the passphrase.
Master key wrapped by passphrase keyNo.Yes, only when the passphrase is enabled. It replaces the PIN-wrapped copy.
SaltsYes.Yes. Salts are not secret.

Enabling the passphrase therefore changes one row: the server-side copy of the master key goes from PIN-wrapped (about 20 bits of protection) to passphrase-wrapped (as strong as the passphrase you chose). Your daily unlock on each device still uses the PIN against the local copy.

05

What our servers store

Our database and file storage are hosted on Supabase. Here is everything in them, grouped by whether we can read it.

Readable by us

  • Email address and a bcrypt hash of your password (held by Supabase Auth), plus sign-in timestamps.
  • Per record: its id, your user id, created and updated timestamps, a deleted flag, a device id and a sync counter.
  • Table names. There is one table per record type, so we can count your firearms, ammunition, accessories, NFA items and so on.
  • Per file: a random file id in a folder named by your user id, the ciphertext size (plaintext plus 28 bytes) and the upload time.
  • Encryption metadata: salts, the wrapped master key(s) and a key version number.
  • Subscription: tier, status, source, period dates, and the identifier Stripe, Apple or Google issued for the purchase.
  • PIN and passphrase lockout counters while a lockout is active. The row is deleted on a successful unlock.
  • On Android only: paywall diagnostic events (event name, product id, error text, app version) used to debug Play Billing. No collection data.

Not readable by us

  • Every field of every record: make, model, serial number, purchase price, notes, dates, tax stamp numbers, round counts, maintenance logs, DOPE data, reloading recipes, builds, gear.
  • Photos and documents, including their original file names, MIME types, dimensions and sizes, which travel inside the encrypted payload.
  • Your PIN and passphrase.
  • Your master key.

The qualifier: in default PIN-only mode, "not readable" means "not readable without first brute-forcing the PIN-wrapped key". The next section puts a number on that.

06

Brute force, honestly

Two different attacks

An online attacker guesses PINs through the app on a device that is signed in to your account. An offline attacker has obtained a copy of a wrapped key, from our database, from a backup archive, or from a device's browser storage, and runs the key derivation on their own hardware. Rate limiting only affects the first. Most published figures for ArmoryHub, including some we used to publish ourselves, described only the first.

Online: progressive lockouts

The server cannot verify a PIN, because it never sees one. The app checks the PIN on your device, then reports success or failure to the server, which records failures and enforces a lockout. If the server cannot be reached the app refuses the attempt. PIN and passphrase have separate counters. A successful unlock clears the counter.

3 failures

30 s

5 failures

2 min

7 failures

5 min

10 failures

15 min

15 failures

30 min

20+ failures

1 hour

Because the check happens on the device and the device reports the result, this lockout slows down someone poking at the app. It does not constrain an attacker who has your password, signs in with their own tooling, downloads the wrapped key and works offline. We used to quote "about 57 years" for an online attack. We have removed that figure because it measured the app's rate limit, not the protection of your data.

Offline: what actually protects the key

Against an offline attacker the only things that matter are the entropy of the secret and the cost of each guess. Time to try every possibility, assuming a modern GPU performs about 20,000 PBKDF2-SHA256 derivations per second at 310,000 iterations:

SecretEntropyOne GPU10,000 GPUs
6-digit PIN (default)≈ 20 bits (1,000,000 values)≈ 1 minuteunder a second
4 random diceware words≈ 52 bits≈ 6,000 years≈ 7 months
6 random diceware words (if you enable one)≈ 78 bits≈ 350 billion years≈ 35 million years

Passphrase rows use 600,000 iterations, so halve the guess rate. The numbers are rounded and assume the words are chosen at random from a 7,776-word list. A passphrase made of a quotation, a name or a pattern has far less entropy than its length suggests.

The conclusion is simple and we would rather state it than have you work it out: in default mode, the PIN is a convenient unlock code for your devices, and your account password is the only thing standing between an attacker and an offline attack on the server-side copy of your key. The passphrase is what makes the server-side copy genuinely unreadable.

07

The passphrase

Enable it in Settings. You will type it rarely: when you sign in on a new device, after clearing browser data, or after your login session has fully expired. On each device, once the master key has been fetched and re-wrapped locally with your PIN, daily unlocking is PIN only.

Why it is optional

This is a genuine security trade-off, and we would rather explain it than pick for you. A 6-digit PIN is easy to remember and easy to type every day, but it offers about 20 bits of protection to the copy of your key on our servers. A random six-word passphrase is beyond brute force, but it is not memorable, and we hold no copy of it. If you lose it and have no signed-in device, your collection is gone. Not recoverable by us, not by Supabase, not by anyone, and no support request can change that.

So the passphrase trades one risk for another. It removes the possibility that a stolen copy of our database exposes your records. It adds the possibility that you lose your own records by forgetting or misplacing a string you type a few times a year. For many users the second risk is the larger one. For users whose threat model includes a database breach, or who already run a password manager and keep written backups of important secrets, the first is. Only you know which you are.

Enable it if

  • You use a password manager and will store the passphrase in it.
  • You will also write it down and keep the paper somewhere safe, such as a locked drawer or a fireproof safe.
  • You accept that forgetting it, with no signed-in device left, means permanent loss.

Stay PIN-only if

  • You do not have a reliable place to keep a long random secret.
  • You would rather accept the database-breach risk than the data-loss risk.

What "database breach" actually means here

It does not mean someone guessing your password. The server-side copy of your key is a row in a PostgreSQL database that Supabase runs for us on Amazon Web Services. To get at it an attacker would need one of three things. First, a copy of that database or its backups, which means breaching Supabase or AWS themselves: infrastructure with SOC 2 Type II controls, disks encrypted at rest, no public access to the storage volumes, and a security team far larger than ours. Second, our service-role credentials, which exist only in Vercel's encrypted environment store and the Supabase dashboard, are never shipped to a browser or an app, and can be rotated. Third, a flaw in our row-level security policies, which would expose rows to another signed-in user rather than hand anyone a full dump, and which is the kind of bug our own review looks for.

None of those is impossible, which is why the passphrase exists. None is likely for an individual collector, which is why it is optional. Weigh that against the certainty that a lost passphrase, with no signed-in device left, means your records are gone.

Either way, write down your PIN too. Your records are just as unrecoverable without it.

What changes when you enable it

  • The PIN-wrapped master key is deleted from the server.
  • A passphrase-wrapped master key is uploaded in its place.
  • Each existing device keeps working with its local PIN-wrapped copy.
  • A new device asks for the passphrase, decrypts the master key, then asks you to set or confirm your PIN and stores a PIN-wrapped copy locally.
  • Changing your PIN no longer uploads a PIN-wrapped key.

What does not change

  • Salts remain on the server. Salts are not secret.
  • The local copy on each device is still PIN-wrapped, so a stolen device whose storage can be read still exposes a PIN-wrapped key. Use full-disk encryption and a strong device passcode.
  • The passphrase is not a recovery method for a forgotten PIN. Neither secret recovers the other. Both are unrecoverable by us.

Choose at least six words picked at random from a diceware list, written down and stored somewhere safe, or kept in a password manager. If you forget the passphrase while a device is still signed in, change it from that device immediately. If you forget it and have no signed-in device, the data cannot be recovered by anyone.

08

Changing your PIN, passphrase or password

Change PIN
Your current PIN unwraps the master key, a new salt is generated, the same master key is re-wrapped under the new PIN key, and a key version counter increases. Other devices notice the new version and ask for the new PIN. The master key itself is not rotated. There is currently no way to rotate the master key short of exporting and re-importing into a fresh account.
Change or disable passphrase
Both require the current passphrase. Changing re-wraps the server copy with a new salt. Disabling re-uploads a PIN-wrapped copy and clears the passphrase columns, returning you to default mode.
Forgot PIN
There is no reset. The "Reset & Start Fresh" option deletes your key material from the device and the server so you can begin again. Your existing records remain in the database as ciphertext that nobody can decrypt, until you use Delete All Data.
Password reset
A link is emailed to you and you set a new password. Nothing else changes. Your password is not part of any key derivation, so resetting it neither exposes nor loses data. After resetting you still need your PIN, and on a new device your passphrase if enabled.
09

Signing in and sessions

Sign-in method
Email and password through Supabase Auth. No social login.
Password rules
Minimum 8 characters, enforced in the app. Use a unique password from a password manager; it is the first line of defence for the server-side key copy.
Two-factor authentication
Not available yet. See Known limitations.
Sessions
Access tokens expire after one hour and refresh automatically while you use the app. Sign-out clears the session and the unwrapped key but leaves the PIN-wrapped key on the device.
After sign-in
Every device still requires the PIN before any record is readable. Signing in alone reveals nothing in the app.
Password hashing
bcrypt, handled by Supabase Auth. We never see the password.
10

On your device

Auto-lock
The unwrapped key is wiped from memory after 5 minutes without activity by default. You can choose 1, 5, 10 or 30 minutes, or never. Locking requires the PIN again.
Browser session
While unlocked, the key is also held in a browser-session key store so a reload does not prompt for the PIN. That store is cleared when the browser session ends. The key is created as an exportable Web Crypto key, so any script running in the page could read it. This is why the Content Security Policy matters.
Biometrics
Face ID, Touch ID and Android biometrics are available in two modes. In "gate" mode the biometric prompt is an extra step before you type the PIN. In "replace" mode your PIN is stored encrypted on the device so the biometric can unlock without typing. In replace mode the protection comes from your device's own security, not from additional cryptography: someone who can read the browser's storage can recover the PIN. Treat it as a convenience.
Decrypted content
Decrypted photos exist only as in-memory blob URLs. The local cache holds encrypted payloads. A browser extension or malware with access to the page can read whatever you can see while the app is unlocked.
Stolen device
If the device is locked, an attacker needs to defeat your device encryption and passcode, then brute-force the PIN-wrapped key found in browser storage, which is about a minute of GPU time. Your device passcode and full-disk encryption are what protect you here, not ArmoryHub.
11

Infrastructure

Application hosting

Vercel

The web app at portal.armoryhub.app and this site are built and served by Vercel. Server-side code (payments, store receipt verification, lockout counters) runs as Next.js API routes there.

Database and files

Supabase on AWS, us-west-2 (Oregon)

PostgreSQL and object storage. Supabase encrypts disks at rest and publishes SOC 2 Type II reports. That is their control, underneath ours.

Row-level security

Enabled on every table

Each policy requires the row's user id to equal the signed-in user. Storage buckets are private and scoped to a folder named by user id. This protects against application bugs leaking one user's ciphertext to another.

Transport

HTTPS only, HSTS preloaded

Strict-Transport-Security with includeSubDomains and preload, two-year max-age. Plain HTTP is never served.

Response headers

CSP with per-request nonce

Content-Security-Policy allows scripts only from our origin, a per-request nonce and Stripe. X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Cache-Control no-store.

Email

Supabase Auth and Resend

Confirmation and reset emails come from Supabase Auth. Welcome and product-update emails are sent through Resend by small functions that receive only your email address.

Earlier versions of this page said "US-East" and "Edge Functions". Both were wrong and were corrected on 10 November 2026.

12

Third-party services

Everything below is a processor of some of your data. None of them receives collection records, because none of them receives anything decrypted. This list is checked against the app's Content Security Policy, which blocks connections to anything not on it.

ServicePurposeWhat it receives
SupabaseDatabase, authentication, file storageEmail, password hash, encrypted payloads and files, and all the metadata listed under What our servers store.
VercelHostingRequest logs (IP address, URL, user agent) as any host does.
StripeWeb paymentsEmail, your ArmoryHub user id, and card details that go directly from your browser to Stripe.
Apple App Store, Google PlayIn-app purchases on iOS, Mac and AndroidPurchase transaction identifiers, which we verify with Apple and Google servers.
ResendWelcome and product-update emailEmail address. Product updates carry an unsubscribe link. Account and security emails are not affected by unsubscribing.

We do not use error-reporting services, session recording, Facebook or other advertising pixels, or AI or image-processing APIs. There are no server-side features that need your plaintext.

13

Trusting the client

Every encrypted web app has the same weak point: the encryption is performed by JavaScript that the server sends you, so you are trusting that code and whoever can change it. We would rather state that plainly than pretend the architecture removes it.

What exists today

  • A Content Security Policy with per-request nonces, so only scripts we ship or explicitly allow can run.
  • Web Crypto API only. There is no bundled cryptography to tamper with.
  • The self-hosted edition runs the same application image, so a self-hoster's instance never loads code from us after install.
  • Source code is made available to security researchers on request. Email support@armoryhub.app.
  • An internal security audit in September 2026, whose open items are published below.

What does not exist yet

  • No independent third-party audit or penetration test. The reason is cost, not reluctance: a credible audit of a system like this runs to tens of thousands of dollars, and ArmoryHub is one developer funded by subscriptions. It will happen when the business can pay for it, and the result will be published here.
  • No public source, reproducible builds or subresource integrity, so you cannot verify the served code matches a reviewed version.
  • Stripe.js loads inside the signed-in app for payments, same-origin with your decrypted data. Our CSP allows it; it does not sandbox it.
14

Threat model

Three columns: what the design protects against, what it partly protects against, and what it does not.

Protected

Database or backup breach, passphrase enabled

The attacker gets ciphertext, salts and a passphrase-wrapped key. Brute-forcing a six-word passphrase is not feasible. They learn record counts and timestamps only.

Depends

Database or backup breach, default PIN-only mode

The attacker gets a PIN-wrapped key and can brute-force it in about a minute per account. Your data is then readable. This is the reason the passphrase exists.

Depends

Legal demand

We can be compelled to hand over what we hold: ciphertext, metadata and the wrapped key. With the passphrase enabled that is unreadable. In default mode a well-resourced party could brute-force the PIN.

Depends

Malicious insider at ArmoryHub or Supabase

Same as a database breach. An insider at ArmoryHub could additionally ship modified client code; see Trusting the client.

Protected

Network interception

Records are encrypted before they leave the device, so a TLS failure exposes ciphertext. Login credentials would be exposed by a TLS failure, as with any site.

Depends

Stolen device, locked

Depends on your device passcode and disk encryption. If those are defeated, browser storage yields a PIN-wrapped key that can be brute-forced offline.

Not protected

Unlocked device or active session

Anyone at your unlocked device sees what you see. Auto-lock reduces the window.

Not protected

Malware or malicious browser extension

Can read decrypted content and the in-session key while the app is unlocked.

Not protected

Phishing of your password and PIN

Someone who obtains both has everything a new device needs in default mode. With the passphrase enabled they also need that.

Not protected

Forgotten PIN and passphrase with no signed-in device

Permanent loss. There is no recovery path, by design.

Not protected

Compromised client delivery

If the code we serve were altered, it could exfiltrate keys. CSP limits who can inject code; it cannot protect you from us or from a compromise of our deployment.

15

Export, deletion and retention

Export (Settings → Export Data)
Produces a ZIP archive. Record payloads and files inside it stay encrypted exactly as they are on the server. It also contains your salts and your wrapped master key(s), so the archive can be restored into any ArmoryHub, hosted or self-hosted, and unlocked with the same PIN and passphrase. The manifest lists your email, user id and record counts in plaintext. The archive is not password-protected. In default mode it contains a PIN-wrapped key, so treat it as sensitive.
CSV reports
The Reports page exports plaintext CSV for spreadsheets, with options to mask serial numbers and prices. It is plaintext because that is its purpose. Store it accordingly.
Deleting items
Deleting a record marks it deleted and keeps the ciphertext so other devices learn about the deletion. Deleted ciphertext is not yet purged on a schedule; it is removed by Delete All Data. A scheduled purge is on the list below.
Delete All Data (Settings)
Permanently deletes every record, photo and document, your device list, profile and encryption metadata, then signs you out. It does not delete your login account, subscription record or lockout counters.
Deleting your account
Email support@armoryhub.app from your account address and we delete the login account and every remaining row. Subscriptions are cancelled through Stripe, the App Store or Google Play as appropriate.
Provider backups
Supabase keeps rolling database backups for a limited period for disaster recovery. Deleted data can persist in those backups until they age out. Everything in them is ciphertext under the same keys described above.
16

Self-hosted edition

The self-hosted edition is the same application image with a different storage layer, so every statement about encryption on this page applies unchanged: records are encrypted on your device with the same key hierarchy before they reach your own database. The differences are about who holds the ciphertext.

  • Your records never reach our servers. The instance makes no outbound requests: no analytics, no telemetry, no error reporting, no licence checks.
  • By default it is reachable only over your Tailscale network. A reverse-proxy mode is available if you run your own.
  • Nightly backups contain ciphertext and your wrapped key, the same as a hosted export.
  • Moving between hosted and self-hosted is one export and one import. Your PIN and passphrase travel with the archive's wrapped key, so nothing is re-encrypted.
  • The application image is private. The install scripts and compose file are public and readable before you run them.

Self-hosting details.

17

Known limitations

These are the open items we know about, mostly from the September 2026 internal audit. Items are listed here as soon as they are known, before they are fixed. Fixed items move to the changelog.

By design

PIN-wrapped master key on the server by default

Default mode leaves a key protected by about 20 bits of entropy in our database and in every export. The passphrase removes it, and is optional for the reasons given in The passphrase: it replaces a breach risk with a data-loss risk, and that choice belongs to the user.

Planned

No two-factor authentication

In default mode your password is the only barrier before an offline attack on the server-side key. TOTP second factor is planned.

Planned

Deleted records are retained as ciphertext

Soft-deleted rows stay until Delete All Data. A scheduled purge is planned.

Documented

Biometric replace mode adds convenience, not cryptography

The PIN is stored encrypted on the device but the key to it is on the same device. Platform keystore-backed storage is being evaluated.

Documented

Rate limiting is enforced on client-reported results

See Brute force, honestly. The lockout is a speed bump for the app, not the protection of your data.

Open

Closed source, no third-party audit

An independent audit costs tens of thousands of dollars, which a one-person subscription business cannot yet justify. The source is available to researchers on request in the meantime. See Trusting the client.

18

Reporting a vulnerability

If you find a security problem in ArmoryHub, the web app, the native wrappers, the self-hosted edition or this site, please tell us before telling anyone else.

Contact
support@armoryhub.app with “Security” in the subject line.
Response
We acknowledge within 72 hours and keep you updated until the issue is resolved.
Safe harbour
Good-faith research that avoids accessing, modifying or destroying other users' data and does not degrade the service will not result in legal action from us.
Please avoid
Testing against accounts you do not own, denial of service, and social engineering of the developer.
Credit
We will credit you in the changelog below if you wish.
19

Changes to this page

  • 10 November 2026

    First version of this page, replacing /features/security and /security-architecture. Corrections relative to the pages it replaces: the server-side copy of the master key is now described accurately as PIN-wrapped by default; the "57 years" online brute-force figure was removed and replaced with offline figures; the hosting region was corrected from US-East to US-West (Oregon); server-side code was corrected from "Edge Functions" to Next.js API routes on Vercel; the processor list was expanded to include Vercel; biometric replace mode and the client-reported nature of rate limiting were documented; a vulnerability disclosure policy was added.