Configuring external services
FeedLog can use four optional external services: OAuth sign-in, transactional email, file storage, and a model endpoint. The instance runs without them, but related features may be disabled or use fallback behavior without producing a startup error. Each section explains what to configure, what happens when the service is not configured, and how to identify common failures. The full variable list is in the repository: docs/configuration.md.
Login providers
Google and GitHub are configured independently. A provider is enabled only when both variables in its pair are present:
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=The sign-in dialog hides providers that are not fully configured. If only one variable in a pair is present, its button does not appear.
If you skip it: email and password sign-in remains available by default.
Email and password sign-in is enabled automatically only when no OAuth provider is configured. If you add Google or GitHub to an existing instance, the password form disappears after the next restart. Set AUTH_EMAIL_ENABLED=true to keep both sign-in methods available.
Callback URLs
Register the callback with the provider exactly as FeedLog will send it:
https://feedback.yourdomain.com/api/auth/callback/google
https://feedback.yourdomain.com/api/auth/callback/githubThe scheme, host, port, and path must all match. Use the public origin through which users access FeedLog. A GitHub OAuth app accepts one callback URL, so a staging environment requires a separate app.
Behind a reverse proxy
By default, FeedLog infers its public origin from the request's Host header and uses X-Forwarded-Proto for the scheme when that header is present. X-Forwarded-Host is not consulted. The origin inferred from the first request after a restart is cached for the lifetime of the process.
This works when the proxy preserves the original host, as Caddy and Traefik do by default. A plain nginx proxy_pass replaces Host with the upstream address. FeedLog will then use that internal address in absolute URLs, including the OAuth redirect_uri, password reset and verification links, and links in notification emails. If a health check reaches the container directly before any public request, FeedLog may cache the container's internal origin.
Preserve the original host at the proxy (proxy_set_header Host $host;), or set the public URL explicitly:
BETTER_AUTH_URL=https://feedback.yourdomain.comDo not include a trailing slash. Setting this value explicitly is recommended for deployments behind a reverse proxy.
Common errors
redirect_uri_mismatch from Google, or "The redirect_uri is not associated with this application" from GitHub — the error page's URL contains the redirect_uri FeedLog sent. Register it verbatim. If it names an internal host or starts with http, fix the proxy, not the OAuth app.
Sign-in completes and lands on the wrong domain — FeedLog inferred the wrong public origin. Set BETTER_AUTH_URL.
The Google or GitHub button isn't there — only one variable in the provider's pair is set.
Password sign-in disappeared after adding OAuth — this is the default when an OAuth provider is configured. Set AUTH_EMAIL_ENABLED=true.
The app won't start after changing AUTH_EMAIL_ENABLED — startup validation fails if email sign-in is disabled and no OAuth provider is configured. An empty value counts as off, not as unset.
Transactional email
RESEND_API_KEY=
EMAIL_FROM=[email protected]
EMAIL_PROVIDER=resend # optional — resend is the only provider todayResend is currently the only implemented provider. EMAIL_PROVIDER selects a registered provider by name, so RESEND_API_KEY is sufficient to register Resend.
Set EMAIL_FROM to an address on a domain you have verified with Resend. Without it, mail is sent from Resend's shared onboarding address, which may only deliver to the account owner.
FeedLog sends these five types of email:
| Sent when | |
|---|---|
| Email verification | A new account is created |
| Password reset | Someone asks for one |
| Password added | A password is set on an account that signed up with OAuth |
| Workspace invitation | An admin invites a member |
| Notification | Feedback someone follows changes status, or gets an official reply |
Notifications go to people following the feedback: its author and anyone who upvoted it. Workspace owners, managers, and the person who caused the event are excluded. Status notifications are sent only when an admin chooses to announce the change; changing a status without announcing it sends no email. FeedLog does not send email for new feedback or new comments.
If you skip it: verification, password reset, and invitation email is not sent. Notifications are written to the log instead of being sent to recipients ([DEV EMAIL] to=… subject=…, followed by the link). No error is returned. Users who cannot sign in cannot recover access through password reset, and admins must copy invitation links manually from the Members page.
AUTH_EMAIL_VERIFY controls whether an email address must be verified before sign-in. When unset, verification is enabled if RESEND_API_KEY is set. Use true or false to override that default.
Common errors
Mail reaches you and nobody else — EMAIL_FROM isn't set, so the shared Resend onboarding sender is in use.
Nothing arrives and no error is shown — look for Registered email provider: resend in the startup log. If the registered provider is console, RESEND_API_KEY is not available to the process.
Signed up, then couldn't sign in — AUTH_EMAIL_VERIFY=true is set without a mail provider. The account exists, but its address cannot be verified because no verification link is sent or written to the log. Password reset is also unavailable.
Links in mail point at the wrong host — FeedLog inferred the wrong public origin from the Host header. Set BETTER_AUTH_URL. Invitation links fall back to http://localhost:3000 when this value is unavailable.
File storage
Feedback and changelog entries support image attachments up to 10 MB each. The storage backend and its configuration depend on the deployment platform; only Docker and Node deployments use the S3_* variables:
| Platform | Backend | What you set |
|---|---|---|
| Docker / Node | S3-compatible, or local disk | S3_* |
| Vercel | Vercel Blob | Connect a Blob store; the token is injected for you |
| Cloudflare Workers | R2 | Bind a bucket as BLOB in wrangler.toml |
Three variables are always required, plus one of two others:
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_BUCKET=feedlog
S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com # non-AWS services
S3_REGION=us-east-1 # AWS S3FeedLog supports S3-compatible services including R2, MinIO, Backblaze, Wasabi, Alibaba OSS, Tencent COS, and Qiniu. Credentials are read at runtime, so restart the application after rotating them; a rebuild is not required.
The bucket does not need to be public. Files are stored under <UPLOAD_PREFIX>/<workspace id>/ and served through FeedLog's /api/files/ route. UPLOAD_PREFIX defaults to uploads and can separate environments that share a bucket. Each file's full path is stored with it, so changing the prefix does not affect existing attachments.
If you skip it on Docker: uploads are written to /app/.data/blob inside the container. The files are removed with the container unless /app/.data is mounted on a volume; the bundled compose.yml includes this mount. Vercel and Workers do not provide a local-disk fallback, so uploads fail without a Blob store or an R2 binding.
Common errors
Uploads work, then vanish after a redeploy — the local-disk fallback is in use and /app/.data isn't on a volume.
S3 is configured but files still go to disk — the provider is registered only when S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, and S3_BUCKET are all present. Missing variables do not produce an error. Confirm that the startup log contains Registered blob provider: s3.
S3 disabled: set S3_REGION (for AWS) or S3_ENDPOINT (for R2/MinIO/OSS) — the three required variables are set but neither of the other two is, and the driver can't build a hostname from that.
Uploads fail against Alibaba OSS — Alibaba OSS may require virtual-hosted style. Keep S3_BUCKET and point S3_ENDPOINT at the bucket host, https://<bucket>.oss-cn-hangzhou.aliyuncs.com.
AI features
OPENAI_API_KEY=
OPENAI_BASE_URL=https://api.openai.com/v1
OPENAI_TEXT_MODEL=OPENAI_API_KEY determines whether AI features are enabled. The other two variables have defaults and are not checked independently, so setting only the base URL does not enable these features.
Three features use the model endpoint, with different behavior when the key is not configured:
| Feature | Without a key |
|---|---|
| Duplicate detection and search | Falls back to trigram text matching |
| AI changelog drafting | Button still there; clicking it returns an error |
| Widget message triage | Message submission fails |
With a key, new feedback is embedded and matched by meaning, so "can't log in with Google" can match "SSO broken". Without a key, search uses PostgreSQL trigram distance over the stored text. It still returns the closest results, but matches wording rather than meaning and can miss duplicates phrased differently. The interface does not indicate which matching method is in use.
The changelog editor continues to show the AI draft button when the key is missing. After a moderator selects a style and starts generation, the dialog shows AI generation is not configured (OPENAI_API_KEY missing).
The feedback widget requires a model endpoint to turn a user's message into structured feedback. Without a key, message submission fails and the end user sees Could not send that. Please try again. Configure the key before enabling the widget.
Feedback created while AI was disabled has no embedding. FeedLog does not create one retroactively, so that feedback is excluded from meaning-based matching until you run the repository's scripts/backfill-search.ts backfill script.
Pointing at a model you run yourself
OPENAI_BASE_URL accepts any OpenAI-compatible endpoint, including a LiteLLM or vLLM gateway, Ollama, or a regional provider. If that endpoint runs inside your network, feedback text sent to the model remains inside the network.
The same base URL serves both chat completions and embeddings, so an Azure URL scoped to one deployment can support only one of those requests. FeedLog requests text-embedding-3-large embeddings with 768 dimensions, and the database column also requires 768 dimensions. If the endpoint cannot return that shape, duplicate detection uses trigram text matching instead. OPENAI_TEXT_MODEL selects only the chat model and does not affect embeddings.
Common errors
"AI generation is not configured" in the changelog editor — OPENAI_API_KEY is not available to the process. On Workers it is a secret (wrangler secret put OPENAI_API_KEY), not a [vars] entry.
Older feedback is missing from semantic duplicate suggestions after adding a key — it was created without an embedding. Run the backfill script.
A gateway 404s on one feature and works on the other — the base URL is scoped to a single deployment. Point it at a gateway that routes both /chat/completions and /embeddings.
Widget messages fail with only a generic error on screen — check the server log for the underlying error.