Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/www/app/docs/self-hosting/aws/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ The load balancer should only send traffic to backends where
`/health/system` returns `{"wal":"ok"}`. Open the dashboard and create an app to
check queries, writes, realtime updates, and file uploads.

Until Postmark is configured, login codes are written to the backend logs. For
an ECS deployment using CloudWatch Logs, tail the log group configured on the
backend task definition:
Until an email provider is configured, login codes are written to the backend
logs. For an ECS deployment using CloudWatch Logs, tail the log group configured
on the backend task definition:

```shell {% showCopy=true %}
aws logs tail /your/backend/log-group \
Expand Down
2 changes: 1 addition & 1 deletion client/www/app/docs/self-hosting/migrate/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ create an app, query it, and write data.

Before restoring your app:

- [Configure Postmark](/docs/self-hosting#configure-email-with-postmark) so magic code emails work.
- [Configure an email provider](/docs/self-hosting#configure-email-with-postmark) so magic code emails work.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- [Restrict dashboard signups](/docs/self-hosting#restrict-dashboard-signups) and [disable temporary apps](/docs/self-hosting#temporary-apps) to prevent unwanted app creation.
- Configure webhooks for your self-hosted app if your app uses them.

Expand Down
29 changes: 27 additions & 2 deletions client/www/app/docs/self-hosting/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ startup. For side projects the expected cost is at least $30/month. For serious
## Operating

Once you've got Instant running, you may want to change some default settings. At
the very least you should be sure to configure Postmark so magic code emails can
actually be delivered.
the very least you should be sure to configure an email provider so magic code
emails can actually be delivered.

### Configure email with Postmark

Expand All @@ -47,6 +47,31 @@ INSTANT_TEAM_EMAIL_SENDER_EMAIL=teams@example.com
Restart the backend and try logging in to the dashboard. If all goes right, you
should get an email delivered!

### Configure email with SendGrid

You can use [SendGrid](https://sendgrid.com/) instead of Postmark. Create an API
key, verify your sender addresses (or authenticate your domain), and set:

```shell
SENDGRID_TOKEN=replace-with-your-api-key
INSTANT_EMAIL_REPLY_TO=hello@example.com
INSTANT_DASHBOARD_EMAIL_SENDER_NAME=Instant
INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL=verify@example.com
INSTANT_APP_EMAIL_SENDER_NAME=Instant
INSTANT_APP_EMAIL_SENDER_EMAIL=verify@example.com
INSTANT_TEAM_EMAIL_SENDER_NAME=Instant
INSTANT_TEAM_EMAIL_SENDER_EMAIL=teams@example.com
```

If only one of `POSTMARK_TOKEN` or `SENDGRID_TOKEN` is set, Instant uses that
provider automatically. If you set both, choose one explicitly with:

```shell
INSTANT_EMAIL_PROVIDER=sendgrid
```

Restart the backend and try logging in to the dashboard to confirm delivery.

### Configure Google dashboard login

The dashboard also allows for login via Google. To enable this, you'll need to create a Web application OAuth client in the
Expand Down
4 changes: 2 additions & 2 deletions client/www/app/docs/self-hosting/vps/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ curl -fsS https://api.myinstant.com/health/system
A healthy backend returns `{"wal":"ok"}`. Open the dashboard and create an app
to check queries, writes, and file uploads.

Until Postmark is configured, login codes are written to the backend logs. Tail
them with:
Until an email provider is configured, login codes are written to the backend
logs. Tail them with:

```sh {% showCopy=true %}
sudo docker compose -f docker-compose.with-caddy.yml --env-file .env logs --follow server
Expand Down
10 changes: 8 additions & 2 deletions self-hosting/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,16 @@ INSTANT_SUPERUSER_EMAIL=
INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_ID=
INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET=

# Email. Provide the Postmark token to send login magic codes.
# If omitted, one-time login codes will be printed to stdout
# Email. Provide a Postmark or SendGrid token to send login magic codes.
# If neither is set, one-time login codes will be printed to stdout
# from the server container.
#
# Set exactly one token, or set both and pick one with INSTANT_EMAIL_PROVIDER
# ("postmark" or "sendgrid"). Make sure the *_EMAIL_SENDER_EMAIL addresses
# below are verified senders/domains with your chosen provider.
POSTMARK_TOKEN=
SENDGRID_TOKEN=
INSTANT_EMAIL_PROVIDER=
INSTANT_EMAIL_REPLY_TO=hello@example.com
INSTANT_DASHBOARD_EMAIL_SENDER_NAME=Instant
INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL=verify@example.com
Expand Down
2 changes: 2 additions & 0 deletions self-hosting/docker-compose.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ services:
INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET: ${INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET:-}
STRIPE_API_KEY: ${STRIPE_API_KEY:-}
POSTMARK_TOKEN: ${POSTMARK_TOKEN:-}
SENDGRID_TOKEN: ${SENDGRID_TOKEN:-}
INSTANT_EMAIL_PROVIDER: ${INSTANT_EMAIL_PROVIDER:-}
INSTANT_EMAIL_REPLY_TO: ${INSTANT_EMAIL_REPLY_TO:-}
INSTANT_DASHBOARD_EMAIL_SENDER_NAME: ${INSTANT_DASHBOARD_EMAIL_SENDER_NAME:-}
INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL: ${INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL:-}
Expand Down
2 changes: 2 additions & 0 deletions self-hosting/docker-compose.with-caddy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ services:
INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET: ${INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET:-}
STRIPE_API_KEY: ${STRIPE_API_KEY:-}
POSTMARK_TOKEN: ${POSTMARK_TOKEN:-}
SENDGRID_TOKEN: ${SENDGRID_TOKEN:-}
INSTANT_EMAIL_PROVIDER: ${INSTANT_EMAIL_PROVIDER:-}
INSTANT_EMAIL_REPLY_TO: ${INSTANT_EMAIL_REPLY_TO:-}
INSTANT_DASHBOARD_EMAIL_SENDER_NAME: ${INSTANT_DASHBOARD_EMAIL_SENDER_NAME:-}
INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL: ${INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL:-}
Expand Down
2 changes: 2 additions & 0 deletions self-hosting/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ services:
INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET: ${INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET:-}
STRIPE_API_KEY: ${STRIPE_API_KEY:-}
POSTMARK_TOKEN: ${POSTMARK_TOKEN:-}
SENDGRID_TOKEN: ${SENDGRID_TOKEN:-}
INSTANT_EMAIL_PROVIDER: ${INSTANT_EMAIL_PROVIDER:-}
INSTANT_EMAIL_REPLY_TO: ${INSTANT_EMAIL_REPLY_TO:-}
INSTANT_DASHBOARD_EMAIL_SENDER_NAME: ${INSTANT_DASHBOARD_EMAIL_SENDER_NAME:-}
INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL: ${INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL:-}
Expand Down
2 changes: 2 additions & 0 deletions self-hosting/swarm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ services:
INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET: ${INSTANT_DASHBOARD_GOOGLE_OAUTH_CLIENT_SECRET:-}
STRIPE_API_KEY: ${STRIPE_API_KEY:-}
POSTMARK_TOKEN: ${POSTMARK_TOKEN:-}
SENDGRID_TOKEN: ${SENDGRID_TOKEN:-}
INSTANT_EMAIL_PROVIDER: ${INSTANT_EMAIL_PROVIDER:-}
INSTANT_EMAIL_REPLY_TO: ${INSTANT_EMAIL_REPLY_TO:-}
INSTANT_DASHBOARD_EMAIL_SENDER_NAME: ${INSTANT_DASHBOARD_EMAIL_SENDER_NAME:-}
INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL: ${INSTANT_DASHBOARD_EMAIL_SENDER_EMAIL:-}
Expand Down
16 changes: 14 additions & 2 deletions server/src/instant/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@
(some-> @config-map :postmark-token crypt-util/secret-value)))

(defn sendgrid-token []
(some-> @config-map :sendgrid-token crypt-util/secret-value))
(or (some-> (System/getenv "SENDGRID_TOKEN") string/trim not-empty)
(some-> @config-map :sendgrid-token crypt-util/secret-value)))
Comment thread
coderabbitai[bot] marked this conversation as resolved.

(defn postmark-account-token []
(or (System/getenv "POSTMARK_ACCOUNT_TOKEN")
Expand All @@ -158,7 +159,18 @@
:email (or (System/getenv "INSTANT_TEAM_EMAIL_SENDER_EMAIL")
"teams@pm.instantdb.com")})

(defn sendgrid-send-disabled? []
(defn email-provider
"Explicit email-provider override for self-hosted deployments
(INSTANT_EMAIL_PROVIDER = \"postmark\" | \"sendgrid\"). Wins over token
auto-detection when set. nil when unset."
[]
(some-> (System/getenv "INSTANT_EMAIL_PROVIDER")
string/trim
string/lower-case
not-empty
keyword))
Comment thread
coderabbitai[bot] marked this conversation as resolved.

(defn sendgrid-send-enabled? []
(not (string/blank? (sendgrid-token))))

(defn postmark-send-enabled? []
Expand Down
24 changes: 22 additions & 2 deletions server/src/instant/email_router.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
(ns instant.email-router
(:require
[instant.config :as config]
[instant.flags :as flags]
[instant.postmark :as postmark]
[instant.sendgrid :as sendgrid]))
Expand All @@ -9,7 +10,26 @@
"verify@dash-pm.instantdb.com" "verify@auth-sg.instantdb.com"})

(defn send-structured! [req]
(if (and (flags/send-with-sendgrid?)
(contains? sendgrid-froms (-> req :from :email)))
(cond
;; Explicit provider override (self-hosted), wins even if both tokens
;; are configured.
(= :sendgrid (config/email-provider))
(sendgrid/send! req)

(= :postmark (config/email-provider))
(postmark/send-structured! req)

;; Auto-detect: SendGrid configured and Postmark not — route through
;; SendGrid using the operator's own from-address.
(and (config/sendgrid-send-enabled?)
(not (config/postmark-send-enabled?)))
(sendgrid/send! req)

;; Hosted: gated by the feature flag, and only for the known Instant
;; from-addresses that have a SendGrid equivalent.
(and (flags/send-with-sendgrid?)
(contains? sendgrid-froms (-> req :from :email)))
(sendgrid/send! (update-in req [:from :email] sendgrid-froms))

:else
(postmark/send-structured! req)))
11 changes: 6 additions & 5 deletions server/src/instant/sendgrid.clj
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
[instant.postmark :as postmark]))

(defn send! [{:keys [from to cc bcc subject html reply-to]}]
(let [body {:personalizations [{:to

to :cc cc :bcc bcc}]
(let [personalization (cond-> {:to to}
cc (assoc :cc cc)
bcc (assoc :bcc bcc))
body {:personalizations [personalization]
:from from
:reply_to {:email (or reply-to "hello@instantdb.com")}
:reply_to {:email (or reply-to (config/email-reply-to))}
:subject subject
:content
[{:type "text/html" :value html}]}]

(if-not (config/sendgrid-send-disabled?)
(if-not (config/sendgrid-send-enabled?)
(tracer/with-span! {:name "sendgrid/send-disabled"
:attributes body}
(tracer/record-info!
Expand Down
Loading