Skip to content

Commit 5d15dc3

Browse files
Move Together into a standalone app (apps/together) (#523)
* Scaffold standalone Together app and move Together code out of web Create apps/together (Next.js 16) with its own root layout, locale proxy, dictionaries, language context, API/TMDB clients and React Query/sonner providers. Move the Together routes, components, services and tests from apps/web with git mv, drop the /together route prefix and make invite links locale-neutral. Remove the compare/design-preview explorations and the Plotwist session/preferences prefill (default region now follows the page locale). Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Add Together conversion analytics and Plotwist sign-up CTA Add a typed track() helper that forwards to gtag only when it is loaded, inject the gtag script when NEXT_PUBLIC_MEASUREMENT_ID is set, and track room_created, invite_shared, room_joined, voting_started, swipe, match_shown and plotwist_cta_clicked at the existing user actions. Replace the sign-in prompt with a dismissible Plotwist CTA linking to Plotwist sign-up with together UTM tags, and rewrite its copy in all seven locales so it no longer implies an import of saved services. Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Remove Together from web and redirect legacy URLs to the new app Permanently redirect /together, /together/*, /<locale>/together and /<locale>/together/* to NEXT_PUBLIC_TOGETHER_URL (default https://together.plotwist.app), keeping the locale when present and the query string. Drop the Together nav item, sitemap entry, layout chrome exception, dictionary sections and Together dictionary contract tests. Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Allow the standalone Together origin in backend CORS and client guard Add an optional TOGETHER_CLIENT_URL (URL, trailing slash trimmed) to the app config and accept it in production CORS origins and the production Origin/Referer client guard, which would otherwise reject requests from the new domain with 403. Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Wire apps/together into the monorepo and document it Add together#build to turbo (NEXT_PUBLIC_* env, .next outputs) and pass NEXT_PUBLIC_TOGETHER_URL to web#build, exclude the new app's build output from Biome, stop tracking its tsbuildinfo, and add a README, .env.example and a CLAUDE.md entry. Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Track the Next-generated next-env.d.ts for together Match apps/web, which tracks the file exactly as next dev writes it. Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Restore web tsbuildinfo to main Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Pass Together origin to the production backend Co-authored-by: Luiz Henrique <7henrique18@gmail.com> * Pin Node 24.x for Vercel builds of web and together Co-authored-by: Luiz Henrique <7henrique18@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 53f3115 commit 5d15dc3

110 files changed

Lines changed: 1950 additions & 1310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CLAUDE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
Plotwist is a media management app (movies, series, anime) with three main apps in a pnpm/Turbo monorepo:
7+
Plotwist is a media management app (movies, series, anime) with three main apps (plus the `apps/together` sandbox) in a pnpm/Turbo monorepo:
88
- `apps/web` — Next.js 16 web app
99
- `apps/backend` — Fastify 5 REST API
1010
- `apps/ios` — Native SwiftUI iOS app
11+
- `apps/together` — Standalone Next.js MVP sandbox for Together (group movie picking), uses the backend API; run with `pnpm --filter together dev` (port 3001)
1112

1213
## Commands
1314

‎apps/backend/.env.example‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ PORT=3333
55
BASE_URL=http://localhost:3333
66
JWT_SECRET="banana"
77
CLIENT_URL=http://localhost:3000
8+
TOGETHER_CLIENT_URL=http://localhost:3001
89
RATE_LIMIT_MAX=100
910
RATE_LIMIT_TIME_WINDOW_MS=60000
1011
IOS_TOKEN=

‎apps/backend/deploy/index.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ const app = new awsx.classic.ecs.FargateService('aws-host-app', {
137137
cpu: 256,
138138
memory: 512,
139139
portMappings: [httpListener],
140+
environment: [
141+
{
142+
name: 'TOGETHER_CLIENT_URL',
143+
value: 'https://together.plotwist.app',
144+
},
145+
],
140146
secrets: [
141147
{ name: 'APP_ENV', valueFrom: '/plotwist/prod/APP_ENV' },
142148
{ name: 'BASE_URL', valueFrom: '/plotwist/prod/BASE_URL' },

‎apps/backend/src/config.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ function loadAppEnvs() {
5353
const schema = z.object({
5454
APP_ENV: z.enum(['dev', 'test', 'production']).optional().default('dev'),
5555
CLIENT_URL: z.string(),
56+
TOGETHER_CLIENT_URL: z
57+
.string()
58+
.url()
59+
.transform(url => url.replace(/\/+$/, ''))
60+
.optional(),
5661
IOS_TOKEN: z.string().optional().default(''),
5762
PORT: z.coerce.number().default(3333),
5863
BASE_URL: z.string().default('http://localhost:3333'),

‎apps/backend/src/infra/http/client-guard.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ function allowedOrigin(
2121
origin: string | undefined,
2222
referer: string | undefined
2323
): boolean {
24-
const allowed = [...new Set([config.app.CLIENT_URL, ...ALLOWED_ORIGINS])]
24+
const allowed = [
25+
...new Set([
26+
config.app.CLIENT_URL,
27+
config.app.TOGETHER_CLIENT_URL,
28+
...ALLOWED_ORIGINS,
29+
]),
30+
].filter((o): o is string => Boolean(o))
2531
return (
2632
(typeof origin === 'string' && allowed.includes(origin)) ||
2733
(typeof referer === 'string' && allowed.some(o => referer.startsWith(o)))

‎apps/backend/src/infra/http/routes/index.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { config } from '@/config'
1010
import { setInitialCounterValue } from '@/domain/services/shared-urls/set-initial-counter-value'
1111
import { sharedUrlCounterFactory } from '@/infra/factories/shared-url-counter-factory'
1212
import { registerRateLimit } from '../rate-limit'
13+
import { achievementsRoutes } from './achievements'
1314
import { feedbackRoutes } from './feedback'
14-
import { userFavoritesRoutes } from './user-favorites'
1515
import { followsRoutes } from './follow'
1616
import { healthCheck } from './healthcheck'
1717
import { imagesRoutes } from './images'
@@ -20,22 +20,22 @@ import { likesRoutes } from './likes'
2020
import { listItemRoute } from './list-item'
2121
import { listsRoute } from './lists'
2222
import { loginRoute } from './login'
23+
import { userRecommendationsRoutes } from './recommendations'
2324
import { reviewRepliesRoute } from './review-replies'
2425
import { reviewsRoute } from './reviews'
2526
import { sharedUrlRoute } from './shared-url'
2627
import { socialAuthRoutes } from './social-auth'
2728
import { socialLinksRoute } from './social-links'
2829
import { subscriptionsRoutes } from './subscriptions'
2930
import { tmdbProxyRoutes } from './tmdb-proxy'
31+
import { togetherRoutes } from './together'
3032
import { userActivitiesRoutes } from './user-activities'
3133
import { userEpisodesRoutes } from './user-episodes'
34+
import { userFavoritesRoutes } from './user-favorites'
3235
import { userItemsRoutes } from './user-items'
3336
import { userStatsRoutes } from './user-stats'
3437
import { usersRoute } from './users'
3538
import { watchEntriesRoutes } from './watch-entries'
36-
import { userRecommendationsRoutes } from './recommendations'
37-
import { achievementsRoutes } from './achievements'
38-
import { togetherRoutes } from './together'
3939
import { webhookRoutes } from './webhook'
4040

4141
export function routes(app: FastifyInstance) {
@@ -120,9 +120,10 @@ function getCorsOrigin(): true | string[] {
120120

121121
const allowedOrigins = [
122122
config.app.CLIENT_URL,
123+
config.app.TOGETHER_CLIENT_URL,
123124
'https://plotwist.app',
124125
'https://www.plotwist.app',
125-
]
126+
].filter((origin): origin is string => Boolean(origin))
126127

127128
return [...new Set(allowedOrigins)]
128129
}

‎apps/together/.env.example‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
NEXT_PUBLIC_API_URL=http://localhost:3333
2+
NEXT_PUBLIC_APP_URL=http://localhost:3001
3+
NEXT_PUBLIC_PLOTWIST_URL=https://plotwist.app
4+
NEXT_PUBLIC_MEASUREMENT_ID=

‎apps/together/.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
tsconfig.tsbuildinfo

‎apps/together/README.md‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Together
2+
3+
Together is an MVP sandbox for picking tonight's movie as a group: one person
4+
creates a room, shares a locale-neutral invite link, everyone votes and matches
5+
show up. It runs as its own Next.js app so it can be tested on its own
6+
(conversion, retention) before being integrated into Plotwist.
7+
8+
It has no Plotwist session. Rooms, participants and swipes go through the
9+
existing Plotwist backend (`/together/*` endpoints and the `/tmdb` proxy).
10+
11+
## Running locally
12+
13+
```bash
14+
cp apps/together/.env.example apps/together/.env.local
15+
pnpm --filter together dev # http://localhost:3001
16+
```
17+
18+
Other scripts: `pnpm --filter together test`, `typecheck`, `build`.
19+
20+
The backend must be running (`apps/backend`, `make run`) for rooms to work.
21+
22+
## Routes
23+
24+
| Path | Screen |
25+
| ------------------------ | ----------------------------------------- |
26+
| `/[lang]` | Welcome (create or join a room) |
27+
| `/[lang]/[code]` | Room (invite, join, waiting room) |
28+
| `/[lang]/[code]/vote` | Voting deck |
29+
| `/[lang]/[code]/matches` | Matches |
30+
| `/[lang]/[code]/swipe` | Legacy redirect to `vote` |
31+
32+
Paths without a locale (for example invite links like `/ABC123`) are redirected
33+
by `src/proxy.ts` to the locale detected from `Accept-Language`.
34+
35+
## Environment variables
36+
37+
| Variable | Purpose |
38+
| ---------------------------- | ---------------------------------------------------------------- |
39+
| `NEXT_PUBLIC_API_URL` | Plotwist backend URL (required) |
40+
| `NEXT_PUBLIC_APP_URL` | Public URL of this app, used for invite links and metadata |
41+
| `NEXT_PUBLIC_PLOTWIST_URL` | Plotwist web URL for the sign-up CTA (default `https://plotwist.app`) |
42+
| `NEXT_PUBLIC_MEASUREMENT_ID` | Google Analytics ID; analytics are disabled when unset |
43+
44+
## Deploying
45+
46+
Deploy as its own Vercel project with the root directory set to
47+
`apps/together`. The backend must allow this origin: set `TOGETHER_CLIENT_URL`
48+
on the backend to the app's public URL (for example
49+
`https://together.plotwist.app`), otherwise production CORS and the client
50+
guard reject its requests. Plotwist web redirects old `/together` URLs to
51+
`NEXT_PUBLIC_TOGETHER_URL` (default `https://together.plotwist.app`).

‎apps/together/next-env.d.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
import "./.next/dev/types/routes.d.ts";
4+
5+
// NOTE: This file should not be edited
6+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

0 commit comments

Comments
 (0)