Skip to main content
Factrove runs as your own install. The primary path is docker-compose.yml: PostgreSQL, Garage, a one-shot migration, the API, and the web app. That stack builds its images from this repository. GitHub Actions also publishes linux/arm64 images for the API and the web app. See Published images. Factrove is licensed under the GNU Affero General Public License v3.0 or later. If you run a modified version as a network service, offer its source to the people who use it. The web app includes a Source code link. Set SOURCE_CODE_URL on the web process to your fork. The default is https://github.com/magnificode-ltd/factrove. compose.yaml is a different file. It starts PostgreSQL and Garage for pnpm dev and leaves the API and the web app on the host. docker compose up without -f docker-compose.yml selects that file.

Prerequisites

For the Compose stack:
  • Docker Engine and the Compose plugin. The command is docker compose (Compose v2). The file uses health checks, service_completed_successfully, and required ${VAR:?} substitution.
  • A clone of this repository, so Compose can build the images.
  • Enough disk for the build and for the PostgreSQL and Garage volumes.
The stack pulls two pinned images and builds the other two: The API image uses Node.js 24.16.0 and pnpm 11.25.0, the same pins as .nvmrc and packageManager in package.json. You do not install Node on the host unless you also run the CLI or the from-source path. Garage creates the bucket named by S3_BUCKET because its command includes --default-bucket. The API does not create a bucket.

Get the code

docker compose up --build builds factrove-api:local and factrove-web:local from the Dockerfile. Pull requests build those same targets as factrove-api:ci and factrove-web:ci for that check. The images you can pull are in Published images.

Published images

.github/workflows/image.yml publishes two linux/arm64 images on every push to main, and when someone runs that workflow by hand. The runner is ubuntu-24.04-arm, which matches Graviton instances such as t4g in AWS us-east-1. Both images come from the existing Dockerfile. Each image has five tags: A manual run from another branch publishes that commit’s SHA tags, with and without the sha- prefix, and leaves latest where it is. Each image carries org.opencontainers.image.source, org.opencontainers.image.revision (the full commit SHA), and org.opencontainers.image.licenses=AGPL-3.0-or-later. The packages stay private while this repository is private. That is the GitHub Container Registry default. When the repository is open-sourced, set factrove-api and factrove-web to public in each package’s settings. Package visibility is separate from repository visibility. Making a package public is permanent. Pull a private package with a GitHub account that can read it and a token that has the read:packages scope:
Replace latest with a full or short commit SHA, or with sha- and that SHA, to pin a build. factrove-cloud accepts an image tag when it is a release tag containing a dot, a tag starting with sha-, or a sha256 digest. The bare commit SHA tags stay so a reference that already uses them still resolves. The token AWS EC2 will use to pull these images will be stored in AWS Secrets Manager, created by factrove-cloud. docker-compose.yml still builds factrove-api:local and factrove-web:local on the machine running Compose. Published tags are linux/arm64 only. An amd64 host keeps building from this checkout.

Configure

Copy the example and replace every placeholder before the first start. .env.docker is gitignored. The values in the example are placeholders.
Generate a secret with:
Run that once for each value. The output is 64 hex characters. Use it as-is.
  • POSTGRES_PASSWORD is inserted into DATABASE_URL without encoding. @, :, /, and # break that URL. The hex output avoids those characters.
  • BETTER_AUTH_SECRET must be at least 32 characters. The hex output qualifies. It signs session cookies.
  • GARAGE_RPC_SECRET must be 64 hex characters, which is the hex output above. The example text replace-with-64-hex-characters-from-openssl-rand-hex-32 is not a valid Garage secret. Replace it or Garage exits.
  • S3_SECRET_ACCESS_KEY is the Garage secret key. Use another hex output. S3_ACCESS_KEY_ID is the Garage access key id you choose. This pair is not an AWS key. Compose maps it to AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY on the API.
Compose reads .env.docker only when you pass --env-file .env.docker. A variable in that file reaches a container when docker-compose.yml interpolates it. DATABASE_URL, S3_ENDPOINT, and INTERNAL_API_URL are set in the compose file, so putting those names in .env.docker does not change them.

Variables in .env.docker

PUBLIC_BASE_URL, WEB_ORIGIN, API_PUBLIC_ORIGIN, and BROWSER_API_URL are absolute http or https origins. No userinfo, path, query, or hash. A trailing slash is dropped. Setting PUBLIC_BASE_URL does not start a server or publish the app. docker-compose.yml sets PUBLIC_BASE_URL from ${PUBLIC_BASE_URL:-http://localhost:3000}. An empty value in .env.docker becomes http://localhost:3000 before the API process starts. On this stack the API does not fall back to WEB_ORIGIN. If you change the public origin or WEB_PORT, set PUBLIC_BASE_URL to that same origin. With WEB_ORIGIN=http://localhost:13000 and PUBLIC_BASE_URL empty, viewer and share links still use port 3000. The API uses WEB_ORIGIN for those links only when PUBLIC_BASE_URL is unset, which is the from-source process with the variable left out of apps/api/.env. MARKDOWN_CONVERSION_CONCURRENCY and MARKDOWN_CONVERSION_QUEUE are read with parseInt. A leading integer is kept, so 1.5 becomes 1 and 2abc becomes 2. A value with no leading integer, such as abc, uses the default 2. An empty value uses 2. The integer is then clamped. localhost and 127.0.0.1 are different hosts. A cookie domain cannot cover either. Keep WEB_ORIGIN and PUBLIC_BASE_URL on the same host when either one is localhost or an IP address. When those hosts differ and both are domain names, set AUTH_COOKIE_DOMAIN to the narrowest parent of WEB_ORIGIN, API_PUBLIC_ORIGIN, and PUBLIC_BASE_URL. The API refuses to start when that domain is an IP, a public suffix, or a name that does not cover all three. Rotating SHARE_TOKEN_KEY, or BETTER_AUTH_SECRET when SHARE_TOKEN_KEY is empty, makes existing public links return 404. Share the artifact again afterward. On the published localhost ports the example is already aligned:
If you change WEB_PORT or API_PORT, set WEB_ORIGIN, PUBLIC_BASE_URL, API_PUBLIC_ORIGIN, and BROWSER_API_URL to those host ports. PUBLIC_BASE_URL still needs its own value.

Values the compose file sets

These are not in .env.docker. They are the bundled-stack defaults on the API and web services: FACTROVE_MIGRATE is unset. See Migrations. The web image reads BROWSER_API_URL when it renders a page, so changing that origin does not require a rebuild. Recreate the web container after you edit .env.docker. INTERNAL_API_URL stays http://api:3001 on this network. pnpm build on the host still bakes NEXT_PUBLIC_API_URL for a process you start with next start. See Run from source.

Build and run

From the repository root:
--build builds factrove-api:local and factrove-web:local. -d leaves the stack in the background. Omit -d to attach the terminal to the logs. The same images, built without Compose:
Compose will not start until POSTGRES_PASSWORD, BETTER_AUTH_SECRET, GARAGE_RPC_SECRET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY are set. The :? markers in docker-compose.yml are what print that requirement. The project name is factrove-selfhost. Its volumes are separate from the dev project in compose.yaml, whose name is factrove. Open the library at http://localhost:3000 when WEB_PORT is 3000. The API health check, with no credentials:
A ready API returns {"status":"ok"}. The API image and the api service use that URL as their health check. The web service requests http://127.0.0.1:3000/.
api waits until migrate has exited successfully and until PostgreSQL and Garage are healthy. Garage creates the bucket during its start.

Migrations

Startup of the API process does not migrate. The migrate service runs prisma migrate deploy from the migrations baked into factrove-api:local, then exits. docker compose up creates that container and runs prisma migrate deploy. A later up or up -d starts the service again and runs the deploy again. The process exits when it finishes. restart: "no" stops Compose from bringing that exited container back by itself. The next up still starts the service. Repeating the deploy is safe: a database that already has the migrations reports that none are pending. After you pull new migrations, apply them with the upgrade command before the new API serves:
up -d runs that same deploy again. The explicit run --rm migrate remains the upgrade step. It applies the migrations before you treat the new API as ready. FACTROVE_MIGRATE=1 is an opt-in on the API container: the entrypoint migrates, then serves. Any other value, including empty, serves without migrating. The committed compose file does not set it. Adding the name to .env.docker has no effect until the api service environment contains FACTROVE_MIGRATE. The migrate service is the path this file already runs. There is no down migration. pnpm db:seed is a host command for local fixture identities. The Compose stack does not seed a user. Sign up in the web app.

HTTPS and a reverse proxy

Published ports bind to 127.0.0.1. A reverse proxy on the same machine is the way to serve HTTPS. Terminate TLS at the proxy. The containers speak HTTP. Route the web origin to the web port and the API origin to the API port:
Set the public origins to those URLs. The host ports stay on loopback and do not appear in the public URLs.
BROWSER_API_URL and API_PUBLIC_ORIGIN are the origin the browser uses for the API. WEB_ORIGIN and PUBLIC_BASE_URL are the origin the browser uses for the library, the viewer, and share links. INTERNAL_API_URL stays http://api:3001 so the Next.js server reaches the API on the Compose network. Use the narrowest parent that still covers all three hosts as AUTH_COOKIE_DOMAIN. For library.factrove.example.com and api.factrove.example.com, that parent is factrove.example.com. The browser sends the session cookie to every subdomain of the value. On an https API origin the cookie is named __Secure-better-auth.session_token. On http it is better-auth.session_token. Forward X-Forwarded-For from the proxy. The API trusts that header only when the TCP peer is listed in TRUSTED_PROXIES. Inside the container, a connection that arrived through the published port comes from the Compose bridge gateway, not from 127.0.0.1. Read that gateway after the stack is up:
Set TRUSTED_PROXIES to that address (or a CIDR that contains it). Leave TRUSTED_PROXIES empty when no proxy sends X-Forwarded-For. An empty value counts every visitor as the socket peer, so behind a proxy every visitor shares one rate-limit bucket. Sign-in and public views use that address. The limits are in memory, on each API process. To publish a port beyond loopback, change the mapping in docker-compose.yml from 127.0.0.1:${API_PORT:-3001}:3001 to a wider bind. Keep TLS in front of any interface that is not loopback. Do not log the access or ticket query on content URLs. access is the signed-in viewer grant. ticket is the grant for a password-protected public link. The viewer loads HTML from API_PUBLIC_ORIGIN in an iframe with sandbox="allow-scripts" and without allow-same-origin. There is no separate content hostname. frame-ancestors allows PUBLIC_BASE_URL and WEB_ORIGIN, so those origins must be the ones that frame the API response. Recreate the containers after origin changes. up -d also starts migrate and runs prisma migrate deploy again, which exits once the database is already current:

Object storage

The storage client calls s3:PutObject, s3:GetObject, and s3:DeleteObject on keys the API generates. New objects use {artifactId}/{contentId}.html. Older rows may still use {artifactId}.html. The public filename stays {artifactId}.html. HTTP responses and the CLI omit the key. S3_ENDPOINT is the only custom endpoint the client reads. With that variable unset, AWS_ENDPOINT_URL is ignored. Plain http is accepted for localhost, 127.0.0.1, and ::1. Any other host needs https, unless S3_ALLOW_INSECURE_HTTP=true. That flag must be the string true or false. The committed docker-compose.yml always starts Garage and points the API at http://garage:3900. A different bucket is a change to the api service environment. .env.docker cannot override S3_ENDPOINT while the compose file sets it.

Bundled Garage

Keep the compose file’s endpoint, path style, and S3_ALLOW_INSECURE_HTTP=true. Set S3_BUCKET, S3_REGION, and the Garage key pair as in Configure. Garage creates S3_BUCKET on startup. The S3 port is not published on the host. Clients on the Compose network use http://garage:3900.

AWS S3

The API does not create an AWS bucket. Create the bucket first, in S3_REGION. On the api service:
  • Set S3_BUCKET and S3_REGION.
  • Remove S3_ENDPOINT, S3_FORCE_PATH_STYLE, and S3_ALLOW_INSECURE_HTTP. The client then calls AWS in S3_REGION.
  • Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to the IAM key, or remove both. The client uses that pair whenever both are set, which is what the committed file does with the Garage key. Removing both lets the AWS SDK default chain run inside the container (environment, AWS_PROFILE, or a task role).
  • When the key is temporary, set AWS_SESSION_TOKEN as well. The API sends the token only together with the access key and secret. The committed file does not pass a session token, so add it on the api service.
If you are not starting Garage, remove the garage service and the garage entry under api.depends_on. While that service remains, Compose still requires GARAGE_RPC_SECRET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY.

MinIO and other S3-compatible stores

MinIO and any other S3-compatible store use the same client. The API does not create the bucket. On the api service:
  • Set S3_BUCKET, S3_REGION, and S3_ENDPOINT to that store’s origin. No path, query, hash, or credentials in the URL.
  • Set S3_FORCE_PATH_STYLE to true for path-style buckets (MinIO’s usual mode). Virtual-hosted buckets use false. When the endpoint is set and this variable is unset, the client uses path style.
  • Use https for a host other than loopback. S3_ALLOW_INSECURE_HTTP=true allows http to a private-network hostname. The access key is sent in cleartext.
  • Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to that store’s key pair.
Remove the bundled garage service the same way as for AWS when you are not using it.

External PostgreSQL

The committed file always starts postgres:17.11 and sets DATABASE_URL on migrate and api to postgresql://factrove:<POSTGRES_PASSWORD>@postgres:5432/factrove. An external database is a change to those two DATABASE_URL values. A DATABASE_URL line in .env.docker does not replace them. Point both services at the external URL. Encode reserved characters in the password. Remove the postgres service and the postgres entries under depends_on, or Compose will keep starting the bundled database. The migrations are the committed SQL in apps/api/prisma/migrations/, applied by the migrate service. The database user needs permission to create tables and indexes. This repository runs those migrations on PostgreSQL 17.

Point the CLI at this install

Install the published CLI and point it at this API. Node.js must be at least 24.7.0 and below 25. The package name is factrove. ftrv is the same command.
npx factrove configure --api-url http://localhost:3001 skips the global install. The built-in origin is https://api.factrove.app, so this install needs --api-url or a saved profile URL. --endpoint-url is the same option. --api-key prompts when you omit it, so the key stays out of shell history. Create the key in the web app at /account/api-keys after you sign up. The secret is shown once. The same session call is in API overview. From a checkout, the same command is pnpm factrove. Node.js 24.16.0 and pnpm 11.25.0 match the pins in this repository.
factrove configure writes ~/.factrove/credentials and ~/.factrove/config mode 0600 and stores the origin as api_url. It does not print the key. Later commands use that profile. Precedence is in CLI. pnpm factrove can also read FACTROVE_API_URL from apps/cli/.env when the process environment and the profile have no URL. The example sets that URL to http://localhost:3001. node apps/cli/dist/main.js and the published commands do not read that file. An explicit --profile uses that profile’s key and ignores FACTROVE_API_KEY.

Check that it works

With the stack up and the origins left on localhost:
1

Health

The body is {"status":"ok"}.
2

Sign up

Open http://localhost:3000/signup and create an account. Passwords are 8 to 128 characters. Sign-up also signs you in.
3

API key and the CLI

Create a key at http://localhost:3000/account/api-keys, then from the repository root:
Plain upload prints only the artifact id. pnpm writes the script line it runs to stderr, so the substitution above is the id.
4

Library and viewer

Open http://localhost:3000, choose My artifacts, and use Refresh if the new row is not listed yet. The viewer is http://localhost:3000/artifacts/{id}. An empty My artifacts page shows npm i -g factrove, npx factrove, the ftrv alias, factrove configure with the API origin, and factrove upload, plus a prompt for a coding agent. The commands on this page use pnpm factrove because they run from the checkout. Plain upload prints the artifact id. Add --json and open viewerUrl from that line.
5

Share link

The command prints a URL on PUBLIC_BASE_URL, shaped http://localhost:3000/share/{id}/{token}. Open that URL without an account. It serves the stored HTML until you revoke it.
Upload failures are in Troubleshooting. A wrong API origin is NETWORK_ERROR. A storage mismatch is STORAGE_UNAVAILABLE, and that failed upload leaves no artifact row.

Upgrade

From the repository root, with the same .env.docker:
build refreshes factrove-api:local and factrove-web:local. run --rm migrate is the deliberate migration step. It applies committed migrations before the new API serves. up -d recreates the API and the web app and starts migrate again, which runs the same deploy. Both runs are safe to repeat. Keep BETTER_AUTH_SECRET and any SHARE_TOKEN_KEY stable across the upgrade, or existing sessions and share links stop resolving. Stop writers before a migration that rewrites artifact if you are crossing 20261021170000_add_artifact_object_key from an older build. That migration locks artifact. An API process from before it cannot insert or replace content once object_key is required. There is no down migration. Rolling back that cutover means stopping the API, copying each current object to {artifactId}.html in the same bucket, and restoring the previous schema and application. The previous application reads {artifactId}.html.

Backups

Take the database, the object bytes, and the key material together. A successful replacement deletes the previous object after the new one is published, so a later database row no longer names the bytes you replaced. Stop the API and the web app so a replacement cannot delete an object during the copy:
Dump PostgreSQL from the bundled service. Inside that container, local connections use trust authentication, so pg_dump accepts user factrove without a password:
The named volume for that data is factrove-postgres on project factrove-selfhost (Docker stores it as factrove-selfhost_factrove-postgres). A volume copy is a second copy of the same data. Stop postgres before you archive the volume, so PostgreSQL is not writing it. pg_dump is the copy you can restore into a new database. For the bundled bucket, stop Garage and archive both of its volumes. Metadata is SQLite, so a copy taken while Garage is running can tear.
The volume names in the compose file are factrove-garage-meta and factrove-garage-data. Docker stores them as factrove-selfhost_factrove-garage-meta and factrove-selfhost_factrove-garage-data. List them with:
On AWS or MinIO, copy the bucket with that store’s tools. Include every object named by the current rows:
Store BETTER_AUTH_SECRET, any SHARE_TOKEN_KEY, GARAGE_RPC_SECRET, and the S3 key pair with the backup, outside the dump. Sessions and share tokens cannot be read without the same auth material. When SHARE_TOKEN_KEY is unset, the share key is derived from BETTER_AUTH_SECRET. Keep GARAGE_RPC_SECRET with that backup. Restore by loading that dump and those object bytes, then starting the stack with the same secrets. The dump does not include rows written after it, and it does not recreate objects a later replacement deleted. Start the stack again with docker compose --env-file .env.docker -f docker-compose.yml up -d. That starts migrate again. prisma migrate deploy leaves a database that already contains those migrations unchanged.

Run from source

Use this path when you run the Node processes yourself. PostgreSQL 17 and an S3-compatible bucket still have to be reachable from the API. Node.js must satisfy >=24.7.0 <25 (24.16.0 is pinned). That floor is the first release with crypto.argon2, which share passwords use. pnpm must satisfy >=11.25.0 <12 (11.25.0 is pinned). engineStrict in pnpm-workspace.yaml enforces both ranges. pnpm infra:up can start the dev PostgreSQL and Garage from compose.yaml (host ports 127.0.0.1:5432 and 127.0.0.1:3900, bucket factrove, local key factrove-local). A database and bucket you already run are configured only through apps/api/.env.
Edit apps/api/.env. The API loads that file on startup and keeps variables that are already set in the environment. The example matches compose.yaml. BASE_URL is read by pnpm db:seed and by auth schema generation. The API process uses API_PUBLIC_ORIGIN instead. pnpm dev does not copy BROWSER_API_URL, INTERNAL_API_URL, or SOURCE_CODE_URL out of apps/api/.env. For a production web process, export them in the environment. Set NEXT_PUBLIC_API_URL before the build when you are not using BROWSER_API_URL. SOURCE_CODE_URL is read when a page renders, including under next start.
pnpm build and next build fix NEXT_PUBLIC_API_URL into the client bundle. next start cannot change that baked origin. BROWSER_API_URL on the web process replaces it for pages rendered after you set the variable.
Apply migrations, build, and start. Startup does not migrate. Run each start command under a process supervisor. The working directory can be the repository root. The API reads apps/api/.env itself.
pnpm --filter @factrove/api start runs node apps/api/dist/main.js. pnpm appends --hostname and --port to next start. An extra -- before them makes Next.js treat --hostname as a directory and the process exits. Bind both to loopback when the proxy is on the same machine. Behind that proxy, TRUSTED_PROXIES=127.0.0.1 matches a proxy that connects to the host loopback. That value is for this host process. The container path in HTTPS and a reverse proxy sees the bridge gateway instead. pnpm db:seed upserts the fixture user local-user and organization local-org. That user has no password. It is optional and it does not wipe artifacts. A real install signs up in the web app. That sign-up sends the invite email. Seed does not.

Invite email

Creating an account sends one invite email to that address. The signup page and POST /api/auth/sign-up/email are the same path. The CLI does not create accounts. There is no organization-member invite route. The invite depends on EmailSender. EMAIL_TRANSPORT picks the implementation. log writes the message and does not send it. smtp sends through any SMTP server. brevo posts https://api.brevo.com/v3/smtp/email. An unknown transport, or a missing setting for the transport you chose, stops the process before it listens. Startup does not open a network connection and does not call GET /v3/account. EMAIL_FROM_ADDRESS defaults to support@factrove.app. EMAIL_FROM_NAME defaults to Factrove. docker-compose.yml passes the same names. An empty SMTP block with EMAIL_TRANSPORT=log is valid. A leftover BREVO_API_KEY is ignored unless the transport is brevo. The message greets the person with “Hi ,” or “Hi,” when the name is blank, then says the Factrove account for that address is ready. A “Sign in to Factrove” button links to WEB_ORIGIN/login, with that same URL on its own line. The footer says why the message was sent, gives EMAIL_FROM_ADDRESS as the support contact, and links to https://factrove.app. The HTML and the plain-text part carry the same words. Nothing in the template adds a query parameter. Set WEB_ORIGIN to the public URL of the web app before you send mail. The sign-in link is that origin plus /login. The default http://localhost:3000 only works for a browser on the same machine. Sign-up does not wait for the provider. The API sends at most four invites at once and keeps at most 100 more waiting. A sign-up past that limit still creates the account. The log says Invite email skipped for <address>. The send queue is full. and that invite is not sent. On shutdown the API waits up to 10 seconds for queued and in-flight invites, then logs Invite email shutdown left N send(s) unfinished: followed by those addresses. pnpm dev force-stops the API after 15 seconds, and the Compose API service sets stop_grace_period: 15s, so that drain can finish. The HTML and plain-text layout live in apps/api/src/mail/invite-email-template.ts. Edit that file, then rebuild the API, to change the wording, colors, or layout. EMAIL_LOGO_URL, when set to an absolute http or https URL, replaces the text wordmark with that image. Leave it unset to keep the wordmark. This install needs Node.js 24 (>=24.7.0 <25; 24.16.0 is pinned). A ready API answers GET /api/v1/health. Plus-addresses are valid. The stored address is lower case and the plus tag is kept, so Magnificode+Factrove@Mail.Grokbot.com is the same account as magnificode+factrove@mail.grokbot.com. Send the body as JSON. A form body would turn + into a space. Copy apps/api/.env.example to apps/api/.env when that file is missing. Put one of these blocks in that file before you start the API. BETTER_AUTH_SECRET in the example is long enough for local use. Do not commit BREVO_API_KEY or SMTP_PASSWORD. Log, which is also the default when EMAIL_TRANSPORT is unset:
Any SMTP server. Leave SMTP_USER and SMTP_PASSWORD empty together when that server has no AUTH. SMTP_SECURE=true is implicit TLS, usually port 465. SMTP_SECURE=false is STARTTLS, usually port 587. Leave SMTP_REQUIRE_TLS unset so a plain connection must upgrade. SMTP_REQUIRE_TLS=false allows plaintext: the password and the message are visible on the network. Use that only on a private network.
Brevo’s SMTP relay. The login and the SMTP key come from the Brevo SMTP settings. They are not BREVO_API_KEY.
Brevo’s HTTP API:
From the repository root, with the API stopped:
pnpm infra:up starts PostgreSQL. pnpm db:migrate applies the committed migrations. The API listens on http://127.0.0.1:3001. In a second terminal, from the repository root, the invitee address is the first argument. WEB_ORIGIN is the Origin header and must be the same value the API was started with. The default matches apps/api/.env.example.
A 200 response creates the account. The saved body is /tmp/factrove-signup.json. In the API log:
  • Invite email sent to <address> messageId=<id> means the SMTP server or the Brevo HTTP API accepted the message. messageId is omitted when the provider id contains a secret or is not a normal id.
  • Invite email shutdown left N send(s) unfinished: means the process stopped before those sends finished. The account still exists.
  • EMAIL_TRANSPORT is log. Invite email was not sent. means the log transport is selected. The following lines are From, To, Subject, and the text.
  • Invite email failed for <address>. means the send failed. The account still exists. The log does not include BREVO_API_KEY, SMTP_USER, or SMTP_PASSWORD. Sign-up does not wait for the provider.
A second request for the same address is 422 and does not send another email. Pass a different plus tag to try again, for example send_factrove_invite 'magnificode+factrove2@mail.grokbot.com'. The signup form at http://localhost:3000/signup sends the same request. pnpm dev starts the web app and the API together. Use the curl above when you only need the API. Point the CLI at http://localhost:3001 as in Point the CLI at this install. Health, library, and share checks are the same, against the host ports.

Removing an install

docker compose --env-file .env.docker -f docker-compose.yml down stops the self-host containers and keeps the volumes. down -v also deletes factrove-postgres, factrove-garage-meta, and factrove-garage-data for this project. Objects in a remote bucket stay. pnpm infra:down stops the dev PostgreSQL and Garage from compose.yaml and keeps their volumes. pnpm infra:reset deletes those dev volumes after you confirm. Neither command touches project factrove-selfhost. CLI credentials in ~/.factrove stay until you delete those files. There is no artifact-delete command. A private artifact can be shared with the current organization from the web app, or with PATCH /api/v1/artifacts/{id} and { "accessType": "org_shared", "organizationId": "<current organization id>" }. The CLI does not change access on an existing artifact. See Current limits. Failures during startup are collected in Troubleshooting.