The hosted service: the Next.js app and its cron worker on Vercel (Pro, for the one-minute cron), the domain and its DNS on Vercel, the database on MongoDB Atlas, and blobs and email on AWS (S3 and SES in eu-west-1, defined in infra/cdk). Every step here is a human step; nothing in the repo deploys by itself.
- In the existing cluster, add a database user
pluck-appwith the built-in role readWrite on thepluckdatabase only. - Network access: Vercel functions have no fixed IPs. Either use the Atlas integration for Vercel, or allow
0.0.0.0/0with the strong password and TLS the connection string already uses. - The connection string, with the database name:
mongodb+srv://pluck-app:<password>@<cluster>/pluck?retryWrites=true&w=majority. Indexes are created by the app on first use.
cd infra/cdk
pnpm exec cdk bootstrap aws://<ACCOUNT>/eu-west-1 # once per account and region
pnpm exec cdk diff -c alertEmail=<you>
pnpm exec cdk deploy --all -c alertEmail=<you> --outputs-file cdk-outputs.json # add -c monthlyBudgetUsd=<n> to change the $50 budget, or 0 in an account inside AWS Organizations whose payer has not enabled budgetsThe address for alarms is passed at deploy time so it never lands in the repo. The four stacks:
- PluckEmail: the SES domain identity for getpluck.io with DKIM, a custom MAIL FROM (
mail.getpluck.io), and DMARC, plus two sending subdomains with their own DKIM and MAIL FROM:notify.getpluck.io(sign-in links, shared Lists, report alerts) andupdates.getpluck.io(the Monday email). Each keeps its own reputation, so a spam report on the Monday email never reaches sign-in links or the root domain your own mail uses. Outputs the DNS records. - PluckData: the
pluck-contentbucket (eu-west-1, Infrequent Access after 90 days,inbound/expires after 7 days, CORS PUT from the app origin) and the IAM userpluck-appwith access to the bucket and permission to send from the identity. Its secret key goes to Secrets Manager aspluck/app-user, never to an output. - PluckInbound: the receipt rule set
pluck-inboundthat writes mail forin.getpluck.io(scanned for spam and viruses) to the bucket underinbound/. - PluckAlerts: emails the alert address when SES bounces pass 4% or complaints pass 0.08% (AWS reviews an account at 5% and 0.1%, and can pause sending, which stops sign-in), and when the month's AWS cost passes 80% of the budget or is forecast to pass it.
Then:
- DNS on Vercel. Add every
Dns*output to the domain:node scripts/vercel-dns.mjs infra/cdk/cdk-outputs.json getpluck.ioprints thevercel dns addcommands; add--applyto run them with your Vercel login. The domain belongs to thecoduteam: runvercel switch codufirst, or every add is refused withpermission_denied. - Activate the receipt rule set (CloudFormation cannot): run the
ActivateRuleSetoutput. - SES out of the sandbox: in the SES console, request production access for eu-west-1 (transactional mail: sign-in links and a weekly digest people opt into). Until then SES only sends to verified addresses.
- Confirm the alert subscription: AWS emails the alert address a link; alarms reach it only after you click it.
- The app's keys:
aws secretsmanager get-secret-value --secret-id pluck/app-user --region eu-west-1gives the access key id and secret for Vercel.
- New project from the repo. Root directory
apps/web; framework Next.js; Node 22. Install commandpnpm install --frozen-lockfile; build commandpnpm --filter @pluck/web build. apps/web/vercel.jsonpins functions todub1(next to eu-west-1) and runs/api/worker/tickevery minute (needs Pro).- Environment variables (Production):
| Variable | Value |
|---|---|
MONGODB_URI |
the Atlas string from step 1 |
BETTER_AUTH_URL |
https://getpluck.io (no trailing slash; it is the OAuth issuer and the connector's audience) |
BETTER_AUTH_SECRET |
32+ random bytes (openssl rand -base64 32) |
EMAIL_FROM |
Pluck <no-reply@notify.getpluck.io>: sign-in links (sent with no Reply-To) and shared Lists. Switch to it only once SES shows notify.getpluck.io as verified, or sign-in links stop sending |
DIGEST_FROM |
Pluck <weekly@updates.getpluck.io>, once updates.getpluck.io is verified. Unset, the Monday email goes from EMAIL_FROM |
EMAIL_REPLY_TO |
niall@getpluck.io: a real mailbox, since the sending subdomains receive no mail |
WELCOME_FROM |
Niall at Pluck <niall@getpluck.io>: the welcome note, sent once 27 minutes after sign-up, from a real mailbox (replies come to you). Leave it off and no welcome is sent |
AWS_REGION |
eu-west-1 |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
from pluck/app-user |
S3_BUCKET |
the BucketName output |
CRON_SECRET |
random; Vercel sends it to the cron route |
DIGEST_SECRET |
random (optional; a separate key lets you revoke digest links without signing everyone out) |
ADMIN_EMAILS |
your address |
ABUSE_EMAIL |
the abuse inbox |
EMAIL_IN_DOMAIN |
in.getpluck.io |
APPLE_TEAM_ID |
the Apple Developer team ID that signs the iPhone app (Membership details). Serves /.well-known/apple-app-site-association, so links to /shared/…, /lists/… and /items/… open the app when it is installed |
NEXT_PUBLIC_POSTHOG_KEY |
the PostHog project token (EU project). Read at build time. Turns on the cookie banner, usage analytics after a yes, and error tracking for the browser and the server |
REVIEW_EMAIL, REVIEW_PASSWORD |
the App Review account: an address nobody can guess (apple-review-$(openssl rand -hex 4)@getpluck.io) and a password (openssl rand -base64 18). See "The App Review account" below |
Leave NEXT_PUBLIC_POSTHOG_KEY off Preview, so previews never report into the real project. In PostHog, turn on Error tracking (Project settings, Error tracking) and keep Session replay off: Pluck never sends recordings.
Do not set SMTP_URL, S3_ENDPOINT, S3_PUBLIC_ENDPOINT, ALLOW_PRIVATE_FETCH or AUTH_RATE_LIMIT in production.
Preview deployments build every PR, and the build reads the database, so set MONGODB_URI (a separate preview database, never production's) and BETTER_AUTH_SECRET for the Preview environment too, or every preview fails at "Collecting page data".
- Domain: add
getpluck.ioto the project as the primary domain. Keep/.well-known/*and/mcpfree of apex-to-www redirects (AI tools do not follow them during sign-in). - Firewall (Project, Firewall): add a rate-limit rule on paths starting
/api/auth/, 60 requests a minute per IP, action Deny. The app limits sign-in on its own; this rule turns a flood away before it costs a function run.
- Sign in on getpluck.io with your address (proves SES and the database).
curl https://getpluck.io/.well-known/oauth-protected-resource/mcpnameshttps://getpluck.io/mcp.- Add the connector in an AI tool (see
docs/mcp.md) and ask "what did I pluck this week?". - Forward a newsletter to your
pluck+…@in.getpluck.ioaddress (Settings, Email-in); it lands within a minute. - Watch the Vercel cron log for
/api/worker/tickreturning 200 every minute. - The JWKS (the
jwkscollection) signs every connector token: never delete it.
Store reviewers (Apple's App Review, Google Play's, the Chrome Web Store's) need a username and password and cannot open an email link, so one static review account serves all of them. Keep its address unguessable (review-<random>@getpluck.io) and keep the address and password outside the repo, with whoever submits the builds. With REVIEW_EMAIL and REVIEW_PASSWORD set, that one address signs in with a password and no other can: typing it on the sign-in page on the web (which is also where the Chrome extension signs in) and tapping "Email me a link", or in the iPhone app and tapping "Send my code", shows a Password field instead of sending anything. The account is made at the first password sign-in, and changing REVIEW_PASSWORD changes the password at the next one. Pluck sends no email to the address, and nobody can sign up, reset or change a password.
- Give App Store Connect (App Review Information, Sign-in required) the two values, with the note: "Type the email, tap Email me a link, then type the password."
- Sign in as the account once before submitting and pluck a few real links, a Note and a Todo, tag two and make a List, so the reviewer sees a library and not an empty Inbox.
scripts/demo-seed.mjscannot do this: it needs Mailpit and the local fixture pages, so it only fills a local server. - Unset both to turn it off. The account and its library stay; nothing is deleted.
Move the worker off Vercel cron (one Fargate task running pnpm --filter @pluck/web worker from the same Dockerfile) if long imports or extraction start hitting the 60-second function limit.