i want to buld a unified movie discovery and socia...

Created on: January 28, 2026

Answered using GPT-5.2 Thinking by Chat01

Question

i want to buld a unified movie discovery and social review platform for my final year project help me to build it

Answer

Absolutely — here’s a solid, buildable plan for a unified movie discovery + social review platform that’s realistic for a final-year project, but still impressive.

What you’re building (core idea)

A web app (and optionally mobile) where users can:

  • Discover movies (trending, popular, recommended, by genre/mood)
  • Track/watchlist (“Want to watch”, “Watched”, “Favorites”)
  • Write reviews + rate
  • Follow friends, see a feed of their activity
  • Comment/like reviews, discuss movies
  • Search across movies/people/lists

Think “Letterboxd + IMDb discovery + social feed” in one product.


MVP features (what you should build first)

If you build these well, you have a complete project:

1) Authentication + profiles

  • Sign up / login
  • Profile page: bio, favorite genres, lists, recent activity
  • Privacy setting: public/private profile (optional but nice)

2) Movie discovery (powered by a movie API)

  • Home page:
    • Trending / Popular / Top rated
    • Genre filters
  • Search page: typeahead search
  • Movie details page: cast, synopsis, trailer link, runtime, etc.

3) Social reviews

  • User can:
    • Rate (1–10 or 0.5–5 stars)
    • Write review
    • Add spoilers toggle
    • Edit/delete their review
  • Others can:
    • Like reviews
    • Comment on reviews (optional for MVP)

4) Watchlist / lists

  • “Want to watch”
  • “Watched”
  • “Favorites”
  • Optional: custom lists (“Best thrillers”, “Date night movies”)

5) Friends & feed

  • Follow/unfollow users
  • Feed shows:
    • Friend reviews
    • Friend ratings
    • Friend watchlist additions (optional)

That’s an MVP that looks like a real product.


Recommended tech stack (final-year friendly + modern)

Option A (fastest, clean, widely used)

Frontend: React + Tailwind
Backend: Node.js (Express or NestJS)
DB: PostgreSQL (recommended)
Auth: JWT + refresh tokens (or session cookies)
Movie data: TMDB API (The Movie Database)

Option B (very clean for full-stack)

Frontend: Next.js (React)
Backend: Next.js API routes (or separate Node API)
DB: PostgreSQL
ORM: Prisma
This is a popular “ship fast” stack.

Option C (if you prefer Python)

Backend: Django + Django REST Framework
Frontend: React
DB: PostgreSQL

If you want my suggestion: Next.js + PostgreSQL + Prisma is perfect for a final-year “full product” feel.


High-level architecture

Client (React/Next.js)
↕ REST or GraphQL
Backend API (Node/Django)

PostgreSQL DB

External Movie API (TMDB)

Key idea: you don’t store every movie in your DB.
You store:

  • users
  • reviews
  • ratings
  • follows
  • lists …and reference movies by tmdbId.

Database schema (simple but strong)

Here’s a good baseline:

Users

  • id (uuid)
  • username
  • email
  • passwordHash
  • avatarUrl
  • bio
  • createdAt

Follows

  • followerId
  • followingId
  • createdAt

Reviews

  • id
  • userId
  • tmdbMovieId
  • rating (float/int)
  • title (optional)
  • body
  • hasSpoilers (bool)
  • createdAt, updatedAt

ReviewLikes

  • userId
  • reviewId
  • createdAt

Comments (optional MVP+)

  • id
  • userId
  • reviewId
  • body
  • createdAt

Watchlist / Entries

  • id
  • userId
  • tmdbMovieId
  • status: WANT | WATCHED | FAVORITE
  • createdAt

Lists (optional)

  • id
  • userId
  • name
  • description
  • isPublic
  • createdAt

ListItems

  • listId
  • tmdbMovieId
  • orderIndex

API endpoints (example)

Auth

  • POST /auth/register
  • POST /auth/login
  • POST /auth/logout
  • GET /me

Movies (proxy TMDB)

  • GET /movies/trending
  • GET /movies/search?q=...
  • GET /movies/:tmdbId

Reviews

  • POST /movies/:tmdbId/reviews
  • GET /movies/:tmdbId/reviews
  • GET /users/:userId/reviews
  • PATCH /reviews/:reviewId
  • DELETE /reviews/:reviewId

Social

  • POST /follow/:userId
  • DELETE /follow/:userId
  • GET /feed (friends’ latest activity)

Lists / watchlist

  • POST /watchlist
  • GET /watchlist?status=WANT
  • DELETE /watchlist/:entryId

Recommendation system (final-year “wow factor”)

You can add a simple recommender without ML complexity:

Simple collaborative filtering (practical)

  • Users who rated similar movies highly → recommend movies they liked that you haven’t watched.
  • Steps:
    1. Get users with overlapping rated movies
    2. Compute similarity (cosine or Pearson)
    3. Rank unseen movies by weighted score

Or a simpler MVP+ approach

  • Genre-based personalization:
    • Track genres user watches/likes
    • Recommend trending movies in those genres

If you want, I can give you a fully working algorithm + SQL query approach.


UI pages you should build

  1. Landing / Home (trending, popular)
  2. Search results
  3. Movie details (reviews + add review)
  4. Profile page (lists + reviews)
  5. Feed (friends activity)
  6. Watchlist page
  7. Login/Register

Final-year project deliverables checklist

To make your supervisor happy, include:

  • Use-case diagram + ER diagram
  • System architecture diagram
  • API documentation (Swagger/OpenAPI)
  • Testing: unit tests (backend) + basic UI tests
  • Security: hashing + auth + rate limiting
  • Deployment: Vercel/Netlify frontend + Render/Railway backend + Supabase/Neon Postgres

A realistic 4-week build plan

Week 1

  • UI skeleton + routing
  • Auth + user profile
  • TMDB integration (trending/search/details)

Week 2

  • Reviews CRUD + movie details page
  • Watchlist statuses

Week 3

  • Follow system + feed
  • Likes/comments (optional)

Week 4

  • Recommendations + polish
  • Deploy + documentation + demo video

I can take you step-by-step starting now

Tell me these 3 things and I’ll generate a full project blueprint + folder structure + database schema + API routes tailored to you:

  1. Do you want web only or web + mobile?
  2. Which stack do you prefer: Next.js, MERN, or Django + React?
  3. Do you want the recommender to be simple (genre-based) or collaborative filtering?

If you don’t care, I’ll choose the fastest path and give you a complete starter structure.

Question

mobile app using flutter

Thought

Formulating Flutter app plan and architecture

I’ll provide a Flutter-specific blueprint for the project. The app will use TMDB for discovery and Supabase for user-generated content like profiles, reviews, and watchlists. I’ll cite Supabase docs for Flutter and real-time features. For security, either use Edge Functions to protect the TMDB key or embed it directly with a note that full security isn’t possible. I'll also explain app architecture, screens, and nice-to-have features like real-time feed updates and notifications.

Outlining Flutter app setup and implementation steps

I'll break down the steps for setting up the mobile app. This includes creating a TMDB account, setting up Supabase, creating tables for user data, and configuring RLS policies. I'll also detail setting up the Flutter environment with the required packages, building the TMDB client, and accessing Supabase. I'll include recommendations for the feed and simple recommender system, plus deployment for Android and iOS.

Crafting a concise plan for Flutter + TMDB + Supabase app

I’m putting together the final answer with a structured blueprint for the Flutter + TMDB + Supabase project. I’ll include MVP features, backend schema (Supabase SQL), security considerations, attribution for TMDB, and Flutter project setup with Neat code snippets. Plus, I’ll ensure citations for TMDB auth, rate limiting, Supabase setup, and key Flutter packages. TMDB’s image URL base_url will also be noted.

Thought for 1m 29s

Answer

Nice — Flutter is a great fit for this. Here’s a practical build plan for a unified movie discovery + social review app that you can actually finish for a final-year project, and still looks “real product” level.

The simplest architecture that still feels pro

Data sources

  • TMDB API → discovery data (trending, search, details, cast, posters, etc.)
  • Supabase (Postgres + Auth + Storage + Realtime) → your social layer:
    • users, profiles, follows, reviews, likes, comments, watchlist, activity feed
    • Supabase has a first-class Flutter SDK (supabase_flutter). (Supabase)
    • Realtime “feed updates” is doable using Postgres Changes. (Supabase)

Why this is ideal for a final year project: you avoid writing/hosting a full backend, but you still get a real DB, auth, security rules, and realtime.


MVP feature set (build this first)

  1. Auth + Profile
  • Email/password login (or Google if you want)
  • Profile: avatar, bio, favorite genres
  1. Discovery
  • Trending / Popular / Top Rated
  • Search
  • Movie details page (poster, overview, cast, trailer link)
  1. Social reviews
  • Rating + text review (+ spoiler toggle)
  • Like reviews
  • Comments (optional MVP+)
  1. Watchlist
  • Want to watch / Watched / Favorites
  1. Follow + Feed
  • Follow/unfollow users
  • Feed: friends’ reviews + watchlist activity

IMPORTANT: TMDB attribution (don’t skip)

TMDB requires you to show a notice like:

“This product uses the TMDB API but is not endorsed or certified by TMDB.”
…and put attribution in an “About/Credits” type section. (The Movie Database (TMDB))

Also be mindful of rate limiting/upper limits (don’t spam requests; cache results). (The Movie Database (TMDB))


Database schema (Supabase) — copy/paste SQL

Create these tables in Supabase SQL editor:

sql
-- Profiles (1:1 with auth.users) create table public.profiles ( id uuid primary key references auth.users(id) on delete cascade, username text unique not null, avatar_url text, bio text, created_at timestamptz default now() ); -- Follows create table public.follows ( follower_id uuid references public.profiles(id) on delete cascade, following_id uuid references public.profiles(id) on delete cascade, created_at timestamptz default now(), primary key (follower_id, following_id), check (follower_id <> following_id) ); -- Reviews create table public.reviews ( id bigserial primary key, user_id uuid references public.profiles(id) on delete cascade, tmdb_movie_id int not null, rating numeric(2,1) not null check (rating >= 0 and rating <= 10), title text, body text not null, has_spoilers boolean default false, created_at timestamptz default now(), updated_at timestamptz default now(), unique (user_id, tmdb_movie_id) ); -- Review likes create table public.review_likes ( user_id uuid references public.profiles(id) on delete cascade, review_id bigint references public.reviews(id) on delete cascade, created_at timestamptz default now(), primary key (user_id, review_id) ); -- Comments (optional but nice) create table public.review_comments ( id bigserial primary key, review_id bigint references public.reviews(id) on delete cascade, user_id uuid references public.profiles(id) on delete cascade, body text not null, created_at timestamptz default now() ); -- Watchlist entries create type public.watch_status as enum ('want', 'watched', 'favorite'); create table public.watchlist ( user_id uuid references public.profiles(id) on delete cascade, tmdb_movie_id int not null, status public.watch_status not null, created_at timestamptz default now(), primary key (user_id, tmdb_movie_id, status) ); -- Activity feed events (for a clean feed) create type public.activity_type as enum ('reviewed', 'liked_review', 'watchlist_added'); create table public.activity ( id bigserial primary key, user_id uuid references public.profiles(id) on delete cascade, type public.activity_type not null, tmdb_movie_id int, review_id bigint references public.reviews(id) on delete set null, created_at timestamptz default now() );

RLS (security) basics (minimum viable)

Enable RLS on all tables, then add policies like:

  • anyone can read reviews/profiles
  • only owner can insert/update/delete their reviews/watchlist
  • follows: only follower can create/delete their follow rows

(If you want, I’ll write the exact RLS policy SQL for all tables — it’s a bit long but straightforward.)


Flutter stack (clean + common)

  • State management: Riverpod (Dart packages)
  • Networking: Dio (or http)
  • Routing: go_router
  • Models: freezed + json_serializable
  • Caching: hive (or shared_preferences for light stuff)
  • Backend: supabase_flutter (Supabase)

Suggested folder structure

text
lib/ core/ (theme, constants, utils) data/ (tmdb client, supabase repositories) features/ auth/ discovery/ movie_details/ reviews/ profile/ feed/ watchlist/ router/ main.dart

TMDB integration (how you should do it)

Authentication

Use TMDB v4 “API Read Access Token” as: Authorization: Bearer <token> (The Movie Database (TMDB))

Posters/images

TMDB images require a base_url + size + file_path; you can get base config from /configuration. (The Movie Database (TMDB))

Minimal TMDB client (Dio)

dart
class TmdbClient { TmdbClient(this._dio); final Dio _dio; Future<Response> trendingMovies() { return _dio.get('/trending/movie/day'); } Future<Response> searchMovies(String q) { return _dio.get('/search/movie', queryParameters: {'query': q}); } Future<Response> movieDetails(int id) { return _dio.get('/movie/$id', queryParameters: {'append_to_response': 'credits,videos'}); } }
dart
final dio = Dio(BaseOptions(baseUrl: 'https://api.themoviedb.org/3')) ..interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { options.headers['Authorization'] = 'Bearer ${const String.fromEnvironment("TMDB_TOKEN")}'; options.headers['accept'] = 'application/json'; return handler.next(options); }, ), );

Tip: If you want to avoid shipping the TMDB token in the app, proxy TMDB via a Supabase Edge Function and call that from Flutter (cleaner for demos, and Supabase supports Edge Functions via its platform + Flutter SDK docs mention them). (Supabase)


Supabase usage (what you’ll do in Flutter)

  • Sign up/login via supabase.auth
  • Insert review into reviews
  • Insert activity into activity
  • Feed query:
    • get who I follow
    • select latest activity rows by those users
  • Optionally subscribe to realtime changes for “live feed” (Supabase)

Example “create review” flow:

dart
Future<void> createReview({ required int tmdbMovieId, required double rating, required String body, bool spoilers = false, }) async { final user = supabase.auth.currentUser!; await supabase.from('reviews').insert({ 'user_id': user.id, 'tmdb_movie_id': tmdbMovieId, 'rating': rating, 'body': body, 'has_spoilers': spoilers, }); await supabase.from('activity').insert({ 'user_id': user.id, 'type': 'reviewed', 'tmdb_movie_id': tmdbMovieId, }); }

Screens you should implement (minimum)

  1. Home (Trending / Popular / Top Rated)
  2. Search
  3. Movie Details (+ Reviews list + Add review)
  4. Feed
  5. Profile (reviews, watchlist)
  6. Watchlist
  7. Login/Register
  8. Settings/About (TMDB attribution text) (The Movie Database (TMDB))

If you want a “wow” extra (easy but impressive)

Personalized recommendations (no ML required)

  • Track user’s most watched genres (from TMDB genres of movies they marked watched/favorite)
  • Recommend:
    • trending in top 2–3 genres
    • “similar” movies endpoint on TMDB for movies they liked

This looks smart in a demo and is quick.


What I can do next (pick one and I’ll continue immediately)

  1. Generate full RLS policy SQL for all tables (secure + correct)
  2. Generate a ready-to-run Flutter starter structure (Riverpod + routing + TMDB client + Supabase auth)
  3. Design your ERD + architecture diagram + a clean feature list for your report

Tell me which one you want first, and whether your app is Android only or Android + iOS.

Question

Generate full RLS policy SQL for all tables (secure + correct)

Thought

Designing RLS policies for Supabase tables

I need to create RLS SQL for Supabase tables, ensuring security and correctness. For tables like profiles, follows, reviews, and others, I'll set specific policies. For profiles, I'll limit updates, inserts, and deletes based on ownership, while ensuring public access for SELECT. Similarly, I'll craft rules for follows and reviews to enforce user-level permissions.

Refining RLS policies for additional tables

For updates, I might restrict changes to ensure new.user_id = auth.uid(). For review_likes, comments, and watchlist tables, I'll define permissions such as public SELECT or owner-specific access for certain actions. I'll also consider feed-related activity and decide whether privacy settings should apply to additional details like watchlist status and comments.

Defining activity feed RLS policies

For activity feeds, public access may be restricted, showing only content from friends or followers. I’ll enforce this with RLS by checking if the user is a follower or the event owner. I’ll set permissions for reading, inserting, and editing based on ownership or following status, ensuring security and privacy.

Granting privileges for RLS policies

In Postgres, for RLS policies to work, roles need explicit GRANT statements. Supabase likely grants privileges for public tables by default, but I'll explicitly define GRANT statements to ensure correct permissions for anon and authenticated roles. I'll also enable RLS and create policies for profile actions like SELECT, INSERT, UPDATE, and DELETE.

Finalizing RLS for reviews, activities, and profiles

I’m detailing RLS policies for each table: follows, reviews, comments, watchlist, and activity. I’m specifying permissions for SELECT, INSERT, UPDATE, and DELETE actions based on ownership or following status. I’ll ensure public access where needed, but restrict anonymous users. I also plan to implement policies for profiles and ensure only authorized users update their own data. I wonder if private accounts should prevent access to follow events—need to make a decision there.

Ensuring RLS policies and grants are secure for all tables and roles

To finalize the RLS policies, I'll ensure the policy is idempotent by adding “drop if exists” commands and guaranteeing that users can rerun the script multiple times. I’ll also add security considerations like forcing RLS on certain tables, which helps prevent bypass by table owners, though it’s not mandatory. Additionally, I’ll define clear rules for roles like authenticated and anon. I also have to account for granting permissions on sequences for review creation.

Finalizing RLS policies and grants with user-specific rules

I’ll craft the full script, ensuring the correct table grants for public read (e.g., profiles, follows, etc.) and authenticated-only actions for inserts and updates. I’ll clarify when select permissions should apply to anonymous users and when they shouldn’t. Additionally, I’ll make sure RLS is applied properly with triggers for profile creation and review updates. I'll also restrict activities to prevent user impersonation and ensure reviews use authenticated role where needed.

Finalizing activity and watchlist policies with public access adjustments

For the activity feed and watchlists, I'll adjust permissions to allow followers to access them, using a query with auth.uid(). I’ll also relax the follow permissions to allow public access. RLS will be enabled, but I’ll leave out the force row-level security to avoid conflicts with the service role bypass. I'll provide the final SQL with RLS setup, policy creation, and notes on adjusting privacy settings for profiles and watchlists as needed.

Thought for 2m 12s

Answer

Below is a complete Supabase RLS script for your tables:

  • profiles
  • follows
  • reviews
  • review_likes
  • review_comments
  • watchlist
  • activity

It’s secure by default:

  • Public can read profiles + reviews + likes + comments + follows
  • Only the owner can write/update/delete their own rows
  • Feed (activity) is readable only by you + people who follow you (followers-only feed)

Run this after creating the tables.

sql
-- ========================================= -- GRANTS (needed in addition to RLS policies) -- ========================================= grant usage on schema public to anon, authenticated; -- Public-readable tables grant select on public.profiles to anon, authenticated; grant select on public.follows to anon, authenticated; grant select on public.reviews to anon, authenticated; grant select on public.review_likes to anon, authenticated; grant select on public.review_comments to anon, authenticated; -- Authenticated-only readable tables (RLS further restricts rows) grant select on public.watchlist to authenticated; grant select on public.activity to authenticated; -- Write permissions (RLS will enforce ownership) grant insert, update, delete on public.profiles to authenticated; grant insert, update, delete on public.follows to authenticated; grant insert, update, delete on public.reviews to authenticated; grant insert, update, delete on public.review_likes to authenticated; grant insert, update, delete on public.review_comments to authenticated; grant insert, update, delete on public.watchlist to authenticated; grant insert, update, delete on public.activity to authenticated; -- Sequences for bigserial IDs (reviews, review_comments, activity) grant usage, select on all sequences in schema public to authenticated; -- ========================================= -- ENABLE RLS -- ========================================= alter table public.profiles enable row level security; alter table public.follows enable row level security; alter table public.reviews enable row level security; alter table public.review_likes enable row level security; alter table public.review_comments enable row level security; alter table public.watchlist enable row level security; alter table public.activity enable row level security; -- ========================================= -- PROFILES -- ========================================= drop policy if exists "profiles_select_public" on public.profiles; create policy "profiles_select_public" on public.profiles for select using (true); drop policy if exists "profiles_insert_own" on public.profiles; create policy "profiles_insert_own" on public.profiles for insert to authenticated with check (auth.uid() = id); drop policy if exists "profiles_update_own" on public.profiles; create policy "profiles_update_own" on public.profiles for update to authenticated using (auth.uid() = id) with check (auth.uid() = id); drop policy if exists "profiles_delete_own" on public.profiles; create policy "profiles_delete_own" on public.profiles for delete to authenticated using (auth.uid() = id); -- ========================================= -- FOLLOWS -- ========================================= drop policy if exists "follows_select_public" on public.follows; create policy "follows_select_public" on public.follows for select using (true); drop policy if exists "follows_insert_self" on public.follows; create policy "follows_insert_self" on public.follows for insert to authenticated with check (auth.uid() = follower_id); drop policy if exists "follows_delete_self" on public.follows; create policy "follows_delete_self" on public.follows for delete to authenticated using (auth.uid() = follower_id); -- (Optional) prevent updates entirely by not creating an UPDATE policy -- ========================================= -- REVIEWS -- ========================================= drop policy if exists "reviews_select_public" on public.reviews; create policy "reviews_select_public" on public.reviews for select using (true); drop policy if exists "reviews_insert_own" on public.reviews; create policy "reviews_insert_own" on public.reviews for insert to authenticated with check (auth.uid() = user_id); drop policy if exists "reviews_update_own" on public.reviews; create policy "reviews_update_own" on public.reviews for update to authenticated using (auth.uid() = user_id) with check (auth.uid() = user_id); drop policy if exists "reviews_delete_own" on public.reviews; create policy "reviews_delete_own" on public.reviews for delete to authenticated using (auth.uid() = user_id); -- ========================================= -- REVIEW LIKES -- ========================================= drop policy if exists "review_likes_select_public" on public.review_likes; create policy "review_likes_select_public" on public.review_likes for select using (true); drop policy if exists "review_likes_insert_own" on public.review_likes; create policy "review_likes_insert_own" on public.review_likes for insert to authenticated with check (auth.uid() = user_id); drop policy if exists "review_likes_delete_own" on public.review_likes; create policy "review_likes_delete_own" on public.review_likes for delete to authenticated using (auth.uid() = user_id); -- ========================================= -- REVIEW COMMENTS -- ========================================= drop policy if exists "review_comments_select_public" on public.review_comments; create policy "review_comments_select_public" on public.review_comments for select using (true); drop policy if exists "review_comments_insert_own" on public.review_comments; create policy "review_comments_insert_own" on public.review_comments for insert to authenticated with check (auth.uid() = user_id); drop policy if exists "review_comments_update_own" on public.review_comments; create policy "review_comments_update_own" on public.review_comments for update to authenticated using (auth.uid() = user_id) with check (auth.uid() = user_id); drop policy if exists "review_comments_delete_own" on public.review_comments; create policy "review_comments_delete_own" on public.review_comments for delete to authenticated using (auth.uid() = user_id); -- ========================================= -- WATCHLIST (private by default: only owner can read) -- ========================================= drop policy if exists "watchlist_select_own" on public.watchlist; create policy "watchlist_select_own" on public.watchlist for select to authenticated using (auth.uid() = user_id); drop policy if exists "watchlist_insert_own" on public.watchlist; create policy "watchlist_insert_own" on public.watchlist for insert to authenticated with check (auth.uid() = user_id); drop policy if exists "watchlist_update_own" on public.watchlist; create policy "watchlist_update_own" on public.watchlist for update to authenticated using (auth.uid() = user_id) with check (auth.uid() = user_id); drop policy if exists "watchlist_delete_own" on public.watchlist; create policy "watchlist_delete_own" on public.watchlist for delete to authenticated using (auth.uid() = user_id); -- ========================================= -- ACTIVITY FEED (followers-only + self) -- People can read: -- - their own activity -- - activity of users they follow -- ========================================= drop policy if exists "activity_select_following" on public.activity; create policy "activity_select_following" on public.activity for select to authenticated using ( auth.uid() = user_id OR exists ( select 1 from public.follows f where f.follower_id = auth.uid() and f.following_id = user_id ) ); drop policy if exists "activity_insert_own" on public.activity; create policy "activity_insert_own" on public.activity for insert to authenticated with check (auth.uid() = user_id); drop policy if exists "activity_delete_own" on public.activity; create policy "activity_delete_own" on public.activity for delete to authenticated using (auth.uid() = user_id); -- (Optional) prevent updates entirely by not creating an UPDATE policy

If you tell me whether you want watchlists public on profiles (common for movie apps), I’ll modify the watchlist policies to support:

  • public “Favorites”
  • followers-only “Watched”
  • private “Want to watch” (or any mix you choose)

Question

Generate a ready-to-run Flutter starter structure (Riverpod + routing + TMDB client + Supabase auth)

Question

Generate a ready-to-run Flutter starter structure (Riverpod + routing + TMDB client + Supabase auth)

Thought

Defining the starter setup

Let's define the basics: In pubspec.yaml, we’ll include dependencies like flutter_riverpod ^3.2.0, go_router ^17.0.1, supabase_flutter ^2.12.0, dio ^5.9.1, and optionally json_annotation. If using freezed, we’ll add it with build_runner and json_serializable. I’ll suggest using minimal models without codegen for simplicity but will offer two options: with or without Freezed.

Crafting a minimal setup

Here’s the basic setup:

  • Use Riverpod 3.2.0 for dependency management.
  • Use go_router 17.0.1 for routing.
  • Use supabase_flutter 2.12.0 and Dio for TMDB interactions.

In main.dart, initialize Supabase and run the app with ProviderScope. For environment variables, I’d recommend --dart-define for simplicity, though .env with flutter_dotenv could be an alternative. We also mention alternatives for Android, like enabling cleartext—or not.

Defining TMDB and routing setup

For the TMDB calls:

  • Trending movies: /trending/movie/day
  • Search: /search/movie
  • Movie details: /movie/{id}?append_to_response=credits,videos

I’ll create a TmdbMovie model and use a MovieRepository with TmdbClient. For routing, I'll use go_router for simple nested routes, like /movie/:id and /profile.

To keep things simple in routing, I’ll implement a bottom navigation with IndexedStack.

Configuring GoRouter and auth state management

In GoRouter, I'll set up default routing like GoRoute(path: '/', builder: ...). For deep links, I'll use routes like /movie/:id. I’ll also protect routes, ensuring users are redirected to /login if they're not logged in.

For auth state, I’ll use supabase.auth.onAuthStateChange, handling sessions carefully, and I'll create a custom GoRouterRefreshStream to manage route refreshes, avoiding any additional dependencies.

Implementing app with Supabase and GoRouter

I'll use supabase_flutter's onAuthStateChange for handling authentication and session changes, integrating it with GoRouter to control access to pages like /login or /movie/:id. I’ll create pages for login, registration, discovery, search, movie details, and profile using appropriate providers. Also, I'll define constants for image URLs based on TMDB's doc guidelines and include optional attribution on the Settings page. Let's ensure it all compiles with Dart 3 and maintain a smooth workflow to guide the user through the setup.

Defining app routing and structure

For routing, I'll use MaterialApp.router with Riverpod to provide the appRouterProvider. I'll implement basic validation via AppEnv, manage the TMDB client with Dio, and define models for movies and details. Pages include discovery, search, home shell, and profile, with StatefulShellRoute or internal navigation. Auth is handled with redirects. Finally, I'll ensure proper async state handling with a Riverpod wrapper.

Thought for 3m 30s

Answer

Below is a ready-to-run Flutter starter with:

It includes: Login, Register, Discover (Trending), Search, Movie Details, Profile + Sign out.


1) Create the project

bash
flutter create movie_social cd movie_social

2) pubspec.yaml (replace dependencies section)

Uses the latest versions shown on pub.dev above. (Dart packages)

yaml
dependencies: flutter: sdk: flutter flutter_riverpod: ^3.2.0 go_router: ^17.0.1 dio: ^5.9.1 supabase_flutter: ^2.12.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0

Then:

bash
flutter pub get

3) Run with secrets (recommended: --dart-define)

You’ll run the app like this (replace values):

bash
flutter run \ --dart-define=SUPABASE_URL=https://YOUR_PROJECT.supabase.co \ --dart-define=SUPABASE_ANON_KEY=YOUR_ANON_KEY \ --dart-define=TMDB_TOKEN=YOUR_TMDB_V4_READ_TOKEN

4) Create this folder structure in /lib

text
lib/ main.dart router/app_router.dart core/app_env.dart core/widgets/async_value_view.dart data/supabase/supabase_providers.dart data/tmdb/tmdb_client.dart data/tmdb/tmdb_models.dart data/tmdb/tmdb_providers.dart features/auth/login_page.dart features/auth/register_page.dart features/shell/app_shell.dart features/discovery/discover_page.dart features/discovery/search_page.dart features/movie/movie_details_page.dart features/profile/profile_page.dart

Now paste the code files below.


Code

lib/core/app_env.dart

dart
class AppEnv { static const supabaseUrl = String.fromEnvironment('SUPABASE_URL'); static const supabaseAnonKey = String.fromEnvironment('SUPABASE_ANON_KEY'); static const tmdbToken = String.fromEnvironment('TMDB_TOKEN'); static void validate() { final missing = <String>[]; if (supabaseUrl.isEmpty) missing.add('SUPABASE_URL'); if (supabaseAnonKey.isEmpty) missing.add('SUPABASE_ANON_KEY'); if (tmdbToken.isEmpty) missing.add('TMDB_TOKEN'); if (missing.isNotEmpty) { throw StateError( 'Missing dart-defines: ${missing.join(', ')}.\n' 'Run: flutter run --dart-define=SUPABASE_URL=... --dart-define=SUPABASE_ANON_KEY=... --dart-define=TMDB_TOKEN=...', ); } } }

lib/data/supabase/supabase_providers.dart

dart
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; final supabaseClientProvider = Provider<SupabaseClient>((ref) { return Supabase.instance.client; }); final authStateProvider = StreamProvider<AuthState>((ref) { final client = ref.watch(supabaseClientProvider); return client.auth.onAuthStateChange; }); final sessionProvider = Provider<Session?>((ref) { final client = ref.watch(supabaseClientProvider); return client.auth.currentSession; });

lib/data/tmdb/tmdb_client.dart

dart
import 'package:dio/dio.dart'; class TmdbClient { TmdbClient({ required String token, Dio? dio, }) : _dio = dio ?? Dio(BaseOptions(baseUrl: 'https://api.themoviedb.org/3')) { _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { options.headers['Authorization'] = 'Bearer $token'; options.headers['accept'] = 'application/json'; handler.next(options); }, ), ); } final Dio _dio; Future<Map<String, dynamic>> trendingMovies() async { final res = await _dio.get('/trending/movie/day'); return (res.data as Map).cast<String, dynamic>(); } Future<Map<String, dynamic>> searchMovies(String query) async { final res = await _dio.get( '/search/movie', queryParameters: {'query': query}, ); return (res.data as Map).cast<String, dynamic>(); } Future<Map<String, dynamic>> movieDetails(int id) async { final res = await _dio.get( '/movie/$id', queryParameters: {'append_to_response': 'credits,videos'}, ); return (res.data as Map).cast<String, dynamic>(); } }

lib/data/tmdb/tmdb_models.dart

dart
class TmdbMovie { final int id; final String title; final String? posterPath; final String? overview; final String? releaseDate; final double? voteAverage; TmdbMovie({ required this.id, required this.title, required this.posterPath, required this.overview, required this.releaseDate, required this.voteAverage, }); factory TmdbMovie.fromJson(Map<String, dynamic> json) { return TmdbMovie( id: json['id'] as int, title: (json['title'] ?? json['name'] ?? '') as String, posterPath: json['poster_path'] as String?, overview: json['overview'] as String?, releaseDate: json['release_date'] as String?, voteAverage: (json['vote_average'] as num?)?.toDouble(), ); } } class CastMember { final String name; final String? character; CastMember({required this.name, required this.character}); factory CastMember.fromJson(Map<String, dynamic> json) { return CastMember( name: (json['name'] ?? '') as String, character: json['character'] as String?, ); } } class MovieDetails { final TmdbMovie movie; final List<CastMember> castTop; MovieDetails({required this.movie, required this.castTop}); factory MovieDetails.fromJson(Map<String, dynamic> json) { final movie = TmdbMovie.fromJson(json); final credits = (json['credits'] as Map?)?.cast<String, dynamic>(); final cast = (credits?['cast'] as List?) ?.map((e) => CastMember.fromJson((e as Map).cast<String, dynamic>())) .toList() ?? const <CastMember>[]; return MovieDetails( movie: movie, castTop: cast.take(10).toList(), ); } } /// Simple helper for TMDB posters (works without /configuration). class TmdbImages { static String? posterW500(String? posterPath) { if (posterPath == null || posterPath.isEmpty) return null; return 'https://image.tmdb.org/t/p/w500$posterPath'; } }

lib/data/tmdb/tmdb_providers.dart

dart
import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/app_env.dart'; import 'tmdb_client.dart'; import 'tmdb_models.dart'; final tmdbClientProvider = Provider<TmdbClient>((ref) { return TmdbClient(token: AppEnv.tmdbToken); }); final trendingMoviesProvider = FutureProvider<List<TmdbMovie>>((ref) async { final api = ref.watch(tmdbClientProvider); final data = await api.trendingMovies(); final results = (data['results'] as List).cast<dynamic>(); return results .map((e) => TmdbMovie.fromJson((e as Map).cast<String, dynamic>())) .toList(); }); final searchMoviesProvider = FutureProvider.family<List<TmdbMovie>, String>((ref, query) async { if (query.trim().isEmpty) return const <TmdbMovie>[]; final api = ref.watch(tmdbClientProvider); final data = await api.searchMovies(query.trim()); final results = (data['results'] as List).cast<dynamic>(); return results .map((e) => TmdbMovie.fromJson((e as Map).cast<String, dynamic>())) .toList(); }); final movieDetailsProvider = FutureProvider.family<MovieDetails, int>((ref, id) async { final api = ref.watch(tmdbClientProvider); final data = await api.movieDetails(id); return MovieDetails.fromJson(data); });

lib/core/widgets/async_value_view.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class AsyncValueView<T> extends StatelessWidget { const AsyncValueView({ super.key, required this.value, required this.data, }); final AsyncValue<T> value; final Widget Function(T) data; Widget build(BuildContext context) { return value.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, st) => Center( child: Padding( padding: const EdgeInsets.all(16), child: Text('Error: $e'), ), ), data: data, ); } }

lib/router/app_router.dart

dart
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../features/auth/login_page.dart'; import '../features/auth/register_page.dart'; import '../features/discovery/discover_page.dart'; import '../features/movie/movie_details_page.dart'; import '../features/profile/profile_page.dart'; import '../features/shell/app_shell.dart'; class GoRouterRefreshStream extends ChangeNotifier { GoRouterRefreshStream(Stream<dynamic> stream) { _sub = stream.asBroadcastStream().listen((_) => notifyListeners()); } late final StreamSubscription<dynamic> _sub; void dispose() { _sub.cancel(); super.dispose(); } } GoRouter createRouter(SupabaseClient supabase) { final refresh = GoRouterRefreshStream(supabase.auth.onAuthStateChange); return GoRouter( initialLocation: '/discover', refreshListenable: refresh, redirect: (context, state) { final loggedIn = supabase.auth.currentSession != null; final isAuthRoute = state.matchedLocation == '/login' || state.matchedLocation == '/register'; if (!loggedIn && !isAuthRoute) return '/login'; if (loggedIn && isAuthRoute) return '/discover'; return null; }, routes: [ GoRoute( path: '/login', builder: (context, state) => const LoginPage(), ), GoRoute( path: '/register', builder: (context, state) => const RegisterPage(), ), ShellRoute( builder: (context, state, child) => AppShell(child: child), routes: [ GoRoute( path: '/discover', builder: (context, state) => const DiscoverPage(), ), GoRoute( path: '/profile', builder: (context, state) => const ProfilePage(), ), ], ), GoRoute( path: '/movie/:id', builder: (context, state) { final id = int.parse(state.pathParameters['id']!); return MovieDetailsPage(movieId: id); }, ), ], ); }

lib/features/shell/app_shell.dart

dart
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class AppShell extends StatelessWidget { const AppShell({super.key, required this.child}); final Widget child; int _indexFromLocation(String location) { if (location.startsWith('/profile')) return 1; return 0; // discover default } Widget build(BuildContext context) { final location = GoRouterState.of(context).matchedLocation; final currentIndex = _indexFromLocation(location); return Scaffold( body: child, bottomNavigationBar: NavigationBar( selectedIndex: currentIndex, destinations: const [ NavigationDestination( icon: Icon(Icons.local_movies_outlined), selectedIcon: Icon(Icons.local_movies), label: 'Discover', ), NavigationDestination( icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile', ), ], onDestinationSelected: (i) { if (i == 0) context.go('/discover'); if (i == 1) context.go('/profile'); }, ), ); } }

lib/features/auth/login_page.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/supabase/supabase_providers.dart'; class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); ConsumerState<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState<LoginPage> { final _email = TextEditingController(); final _password = TextEditingController(); bool _busy = false; void dispose() { _email.dispose(); _password.dispose(); super.dispose(); } Future<void> _login() async { setState(() => _busy = true); try { final supabase = ref.read(supabaseClientProvider); await supabase.auth.signInWithPassword( email: _email.text.trim(), password: _password.text, ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e'))); } finally { if (mounted) setState(() => _busy = false); } } Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Login')), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _email, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration(labelText: 'Email'), ), const SizedBox(height: 12), TextField( controller: _password, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), ), const SizedBox(height: 16), FilledButton( onPressed: _busy ? null : _login, child: Text(_busy ? 'Signing in…' : 'Sign in'), ), const SizedBox(height: 12), TextButton( onPressed: _busy ? null : () => Navigator.of(context).pushNamed('/register'), child: const Text('Create an account'), ), ], ), ), ); } }

lib/features/auth/register_page.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/supabase/supabase_providers.dart'; class RegisterPage extends ConsumerStatefulWidget { const RegisterPage({super.key}); ConsumerState<RegisterPage> createState() => _RegisterPageState(); } class _RegisterPageState extends ConsumerState<RegisterPage> { final _email = TextEditingController(); final _password = TextEditingController(); bool _busy = false; void dispose() { _email.dispose(); _password.dispose(); super.dispose(); } Future<void> _register() async { setState(() => _busy = true); try { final supabase = ref.read(supabaseClientProvider); await supabase.auth.signUp( email: _email.text.trim(), password: _password.text, ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Account created. You can now log in.')), ); Navigator.of(context).pop(); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('$e'))); } finally { if (mounted) setState(() => _busy = false); } } Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Register')), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TextField( controller: _email, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration(labelText: 'Email'), ), const SizedBox(height: 12), TextField( controller: _password, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), ), const SizedBox(height: 16), FilledButton( onPressed: _busy ? null : _register, child: Text(_busy ? 'Creating…' : 'Create account'), ), ], ), ), ); } }

lib/features/discovery/discover_page.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/widgets/async_value_view.dart'; import '../../data/tmdb/tmdb_models.dart'; import '../../data/tmdb/tmdb_providers.dart'; import 'search_page.dart'; class DiscoverPage extends ConsumerWidget { const DiscoverPage({super.key}); Widget build(BuildContext context, WidgetRef ref) { final trending = ref.watch(trendingMoviesProvider); return Scaffold( appBar: AppBar( title: const Text('Discover'), actions: [ IconButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const SearchPage()), ), icon: const Icon(Icons.search), ), ], ), body: AsyncValueView<List<TmdbMovie>>( value: trending, data: (movies) => ListView.separated( itemCount: movies.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, i) { final m = movies[i]; return ListTile( leading: const Icon(Icons.movie), title: Text(m.title), subtitle: Text(m.releaseDate ?? '—'), trailing: Text(m.voteAverage?.toStringAsFixed(1) ?? ''), onTap: () => context.push('/movie/${m.id}'), ); }, ), ), ); } }

lib/features/discovery/search_page.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/widgets/async_value_view.dart'; import '../../data/tmdb/tmdb_models.dart'; import '../../data/tmdb/tmdb_providers.dart'; final _queryProvider = StateProvider<String>((ref) => ''); class SearchPage extends ConsumerWidget { const SearchPage({super.key}); Widget build(BuildContext context, WidgetRef ref) { final query = ref.watch(_queryProvider); final results = ref.watch(searchMoviesProvider(query)); return Scaffold( appBar: AppBar(title: const Text('Search')), body: Column( children: [ Padding( padding: const EdgeInsets.all(12), child: TextField( autofocus: true, decoration: const InputDecoration( hintText: 'Search movies…', prefixIcon: Icon(Icons.search), ), onChanged: (v) => ref.read(_queryProvider.notifier).state = v, ), ), Expanded( child: AsyncValueView<List<TmdbMovie>>( value: results, data: (movies) { if (query.trim().isEmpty) { return const Center(child: Text('Type to search.')); } if (movies.isEmpty) { return const Center(child: Text('No results.')); } return ListView.separated( itemCount: movies.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, i) { final m = movies[i]; return ListTile( title: Text(m.title), subtitle: Text(m.releaseDate ?? '—'), onTap: () => context.push('/movie/${m.id}'), ); }, ); }, ), ), ], ), ); } }

lib/features/movie/movie_details_page.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/widgets/async_value_view.dart'; import '../../data/tmdb/tmdb_models.dart'; import '../../data/tmdb/tmdb_providers.dart'; class MovieDetailsPage extends ConsumerWidget { const MovieDetailsPage({super.key, required this.movieId}); final int movieId; Widget build(BuildContext context, WidgetRef ref) { final details = ref.watch(movieDetailsProvider(movieId)); return Scaffold( appBar: AppBar(title: const Text('Movie')), body: AsyncValueView<MovieDetails>( value: details, data: (d) { final m = d.movie; final posterUrl = TmdbImages.posterW500(m.posterPath); return ListView( padding: const EdgeInsets.all(16), children: [ if (posterUrl != null) ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network(posterUrl, height: 280, fit: BoxFit.cover), ), const SizedBox(height: 12), Text( m.title, style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 6), Text('${m.releaseDate ?? ''} • ⭐ ${m.voteAverage?.toStringAsFixed(1) ?? ''}'), const SizedBox(height: 12), Text(m.overview ?? 'No overview.'), const SizedBox(height: 16), Text('Top Cast', style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), ...d.castTop.map((c) => Text('• ${c.name}${c.character != null ? ' as ${c.character}' : ''}')), const SizedBox(height: 24), const Divider(), const SizedBox(height: 8), const Text( 'Reviews coming next: connect this screen to your Supabase reviews table.', ), ], ); }, ), ); } }

lib/features/profile/profile_page.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../data/supabase/supabase_providers.dart'; class ProfilePage extends ConsumerWidget { const ProfilePage({super.key}); Widget build(BuildContext context, WidgetRef ref) { final supabase = ref.watch(supabaseClientProvider); final user = supabase.auth.currentUser; return Scaffold( appBar: AppBar(title: const Text('Profile')), body: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('User: ${user?.email ?? ''}'), const SizedBox(height: 12), FilledButton( onPressed: () async { await supabase.auth.signOut(); }, child: const Text('Sign out'), ), const SizedBox(height: 24), const Text( 'TMDB Attribution: Add an About/Credits screen:\n' '"This product uses the TMDB API but is not endorsed or certified by TMDB."', ), ], ), ), ); } }

lib/main.dart

dart
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'core/app_env.dart'; import 'data/supabase/supabase_providers.dart'; import 'router/app_router.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); AppEnv.validate(); await Supabase.initialize( url: AppEnv.supabaseUrl, anonKey: AppEnv.supabaseAnonKey, ); runApp(const ProviderScope(child: MyApp())); } class MyApp extends ConsumerWidget { const MyApp({super.key}); Widget build(BuildContext context, WidgetRef ref) { final supabase = ref.watch(supabaseClientProvider); final router = createRouter(supabase); return MaterialApp.router( title: 'Movie Social', theme: ThemeData(useMaterial3: true), routerConfig: router, ); } }

5) One tiny fix (Login page register navigation)

In LoginPage, I used Navigator.pushNamed('/register')—but we’re using go_router. Replace that button with:

dart
TextButton( onPressed: _busy ? null : () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const RegisterPage()), ), child: const Text('Create an account'), ),

(or switch it to context.go('/register') if you import go_router).


Next step (so it becomes your “movie social” app)

If you want, I’ll generate the next slice (fully wired to your Supabase tables + RLS you already added):

  1. Create Review screen (rating + body + spoilers)
  2. Movie Reviews list (for a TMDB movie id)
  3. Likes + Comments
  4. Follow + Feed

Say: “Add reviews next” and I’ll drop the exact tables-to-UI code (providers + queries + pages).

Question

Design your ERD + architecture diagram + a clean feature list for your report

Thought

Designing report elements

For the report, I’ll describe the ERD and architecture diagrams textually and use Mermaid diagram syntax. The ERD will include entities like auth.users, profiles, reviews, etc. I'll detail the architecture including Flutter, Supabase, TMDB API, and storage. The feature list will include both core and optional features and highlight admin functionalities. I'll cover RLS, data flow for discovery vs social, and

Thought for 10s

Answer

ERD (Entity–Relationship Diagram)

Entities and key relationships (high level)

  • auth.users (Supabase Auth) → profiles (1:1)
  • profilesreviews (1:N)
  • profileswatchlist (1:N)
  • profilesprofiles via follows (N:N)
  • reviewsprofiles via review_likes (N:N)
  • reviewsreview_comments (1:N)
  • profilesactivity (1:N)
  • TMDB Movie is external; you reference movies by tmdb_movie_id (you do not store full movie rows in your DB)

Mermaid ERD you can paste into your report (or a Mermaid renderer)

mermaid
erDiagram AUTH_USERS ||--|| PROFILES : "1:1 (id)" PROFILES ||--o{ REVIEWS : writes PROFILES ||--o{ WATCHLIST : saves PROFILES ||--o{ ACTIVITY : generates PROFILES ||--o{ FOLLOWS : follower PROFILES ||--o{ FOLLOWS : following REVIEWS ||--o{ REVIEW_COMMENTS : has PROFILES ||--o{ REVIEW_COMMENTS : posts PROFILES ||--o{ REVIEW_LIKES : likes REVIEWS ||--o{ REVIEW_LIKES : receives %% External reference (conceptual) TMDB_MOVIE ||--o{ REVIEWS : "referenced by tmdb_movie_id" TMDB_MOVIE ||--o{ WATCHLIST : "referenced by tmdb_movie_id" TMDB_MOVIE ||--o{ ACTIVITY : "referenced by tmdb_movie_id" AUTH_USERS { uuid id PK text email } PROFILES { uuid id PK "FK -> auth.users.id" text username "unique" text avatar_url text bio timestamptz created_at } FOLLOWS { uuid follower_id PK "FK -> profiles.id" uuid following_id PK "FK -> profiles.id" timestamptz created_at } REVIEWS { bigint id PK uuid user_id "FK -> profiles.id" int tmdb_movie_id numeric rating text title text body boolean has_spoilers timestamptz created_at timestamptz updated_at } REVIEW_LIKES { uuid user_id PK "FK -> profiles.id" bigint review_id PK "FK -> reviews.id" timestamptz created_at } REVIEW_COMMENTS { bigint id PK bigint review_id "FK -> reviews.id" uuid user_id "FK -> profiles.id" text body timestamptz created_at } WATCHLIST { uuid user_id PK "FK -> profiles.id" int tmdb_movie_id PK watch_status status PK "want|watched|favorite" timestamptz created_at } ACTIVITY { bigint id PK uuid user_id "FK -> profiles.id" activity_type type "reviewed|liked_review|watchlist_added" int tmdb_movie_id bigint review_id "nullable FK -> reviews.id" timestamptz created_at } TMDB_MOVIE { int id PK "external" }

Architecture diagram (Flutter + Supabase + TMDB)

Component overview

  • Flutter Mobile App
    • UI screens (Discover, Search, Movie Details, Feed, Profile, Watchlist)
    • Riverpod state management + repositories
    • Local caching (optional: Hive/shared_preferences)
  • TMDB API
    • Discovery/search/movie details, cast, images
  • Supabase Backend
    • Auth (sign up / login)
    • Postgres DB (social data: profiles, reviews, follows, watchlist, feed)
    • Storage (avatars)
    • Realtime (optional: live feed updates)
    • (Optional) Edge Functions to proxy TMDB (hide token, unify API)

Mermaid architecture diagram

mermaid
flowchart LR A[Flutter App\n(UI + Riverpod + Repos)] -->|Discover/Search/Details| B[TMDB API\nMovies + Posters + Cast] A -->|Auth| C[Supabase Auth] A -->|CRUD Social Data| D[Supabase Postgres\nprofiles, reviews, follows, likes,\ncomments, watchlist, activity] A -->|Upload/Download| E[Supabase Storage\nAvatars] D -->|Changes/Subscriptions (optional)| F[Supabase Realtime] F -->|Live updates (optional)| A %% Optional proxy to hide TMDB token + standardize responses A -. optional .-> G[Supabase Edge Functions\nTMDB Proxy / Aggregation] G -. optional .-> B G -. optional .-> D

Data flow (what happens in the app)

  • Discovery flow: Flutter → TMDB (trending/search/details) → UI
  • Social flow: Flutter → Supabase (reviews/likes/comments/follows/watchlist/activity) → UI
  • Unified experience: Movie screens combine TMDB details + Supabase reviews/activity using the same tmdb_movie_id.

Clean feature list for your report

Core functional requirements (MVP)

Authentication & Accounts

  • User registration/login (email + password)
  • Logout
  • Create/update profile (username, bio, avatar)

Movie Discovery (TMDB-backed)

  • Trending movies list
  • Search movies by title
  • Movie details page (overview, rating, release date, cast, posters)

Reviews & Ratings (Social Layer)

  • Create/edit/delete a review for a movie (text + rating)
  • Mark review as spoilers
  • View all reviews for a movie
  • View all reviews by a user

Watchlist / Tracking

  • Add/remove movie to:
    • Want to Watch
    • Watched
    • Favorites
  • View watchlist by status

Social Graph

  • Follow/unfollow users
  • View follower/following counts

Feed

  • Activity feed (e.g., “X reviewed Movie”, “X added to favorites”)
  • Feed limited to self + followed users (via RLS)

Optional enhancements (good for “future work” or MVP+)

  • Comments on reviews + threaded replies
  • Like/unlike reviews + notifications
  • Personalized recommendations (genre-based or collaborative filtering)
  • Lists (custom curated lists) + sharing
  • Advanced filters (genre, year, language, runtime)
  • Offline caching for recently viewed movies
  • Reporting/moderation tools

Non-functional requirements (report-ready)

  • Security: Row Level Security (RLS) on all tables; users can only modify their own content
  • Privacy: Watchlist and feed visibility controlled by policies (e.g., private watchlist, follower-based feed)
  • Performance: paginate lists; debounce search; cache TMDB results
  • Reliability: handle network failures; retry/backoff for requests
  • Scalability: store only social data; reference TMDB IDs to avoid massive movie tables
  • Usability: consistent navigation, clear CTAs (add review / add to watchlist), accessible text sizes

If you want, I can also generate:

  • a 1-page “System Design” write-up (problem statement → objectives → modules → tech justification),
  • and a Use Case diagram list (actors + use cases) that matches this ERD and architecture.

Share this Q&A