What you will build
CineSuper is a movie catalogue like a mini Netflix. Every student builds it with their own database and their own GitHub repo, and deploys it at a public link.
Browse
Grid of movies with poster, year, language and genre.
Search & filter
Search by title, filter by genre.
Details & reviews
Open a movie to see its description and reviews.
Write reviews
Add a 1 to 5 star review saved to the database.
Tech stack
| Layer | Tool | Why |
|---|---|---|
| Database | Supabase (PostgreSQL) | Real SQL database in the browser, free, nothing to install |
| Frontend | HTML, CSS, JavaScript | No frameworks, no build step |
| Code & hosting | GitHub + GitHub Pages | Version history and a free public URL |
Learning outcomes
- Design tables with primary keys, foreign keys and CHECK constraints
- Write DDL (CREATE, ALTER), DML (INSERT, UPDATE, DELETE) and DCL (GRANT)
- Query with JOIN, GROUP BY, HAVING, subqueries and views
- Secure data with Row Level Security (RLS)
- Connect a web page to a cloud database and deploy it publicly
Timeline
Saturday
Phases 0–6 · ~3.5 hrsGit and VS Code setup, repo, database, security, queries
Sunday morning
Phases 7–11 · ~2 hrsFrontend, testing, GitHub Pages, README
Sunday evening
Phase 12 · ~1.5 hrsPersonalisation and final submission
Completion rules
- Every student has their own Supabase project and their own public GitHub repo named
cinesuper-<jsoft-id>. - Every Push checkpoint in this guide must appear as a separate commit in your repo history (8 in total).
- “Push” means saving a commit with Git and sending it to GitHub from the VS Code terminal. Phase 0 shows how to install Git and VS Code and gives the push routine you repeat at every checkpoint.
- The live link must load movies from your Supabase project on the evaluation day.
- The site footer must show your name, JSOFT ID and Jain School of Future Technology.
- Copied repos (same data, same history, no personalisation) get zero.
Use the My push progress panel on the left to tick off each checkpoint as you push it. It is saved in your browser.
Accounts, Git, VS Code and your repo
You set up your tools and your repo first, so every later phase is saved as a commit and pushed to GitHub from the VS Code terminal. Do this once; it takes about 45 minutes.
Step 0.1 — GitHub account
- Go to github.com and click Sign up.
- Use your college email and a professional username, for example
ravi-cse24. - Verify your email.
Step 0.2 — Supabase account
- Go to supabase.com and click Start your project.
- Choose Continue with GitHub and authorise it.
Step 0.3 — Install VS Code
- Go to code.visualstudio.com and download the installer for your computer.
- Windows: run the installer and accept the defaults. On the Select Additional Tasks screen tick Add "Open with Code" action and Add to PATH.
Mac: open the downloaded zip and drag Visual Studio Code into Applications. - Open VS Code. Click the Extensions icon on the left (four squares), search Live Server (by Ritwick Dey) and click Install. You will use it in Phase 9.
Step 0.4 — Install Git
Git for Windows
Download from git-scm.com/downloads/win (64-bit installer) and run it. Click Next on every screen, except:
• Default editor: choose Use Visual Studio Code as Git's default editor
• Initial branch name: choose Override and type main
Command Line Tools
Open the Terminal app, type git --version and press Enter. If Git is missing, a window asks to install the Command Line Tools: click Install and wait. Run git --version again.
Step 0.5 — Open the VS Code terminal and tell Git who you are
- In VS Code, open the menu Terminal → New Terminal (shortcut
Ctrl + `, the key below Esc). A terminal opens at the bottom. - Check Git works. You should see a version number such as
git version 2.47.0.Terminalgit --version - Set your name and the same email you used for GitHub. Type each line, change the values, press Enter. Nothing is printed when it works.
Terminal (run once)
git config --global user.name "Your Full Name" git config --global user.email "your-github-email@example.com" git config --global init.defaultBranch main - Sign in to GitHub inside VS Code: click the Accounts icon (person, bottom-left) → Sign in with GitHub → allow it in the browser. VS Code will then handle your
git pushlogins.
Step 0.6 — Create your repository on GitHub
- On GitHub click + (top right) → New repository.
- Name:
cinesuper-<your-jsoft-id>, for examplecinesuper-jsoft26451. All lowercase. Your JSOFT ID is the one shown next to your name in the submission form. - Visibility: Public (GitHub Pages is free only for public repos).
- Tick Add a README file, then click Create repository.
- On the repo page click the green <> Code button → HTTPS tab → copy the URL. It looks like
https://github.com/ravi-cse24/cinesuper-jsoft26451.git
Step 0.7 — Clone the repo to your laptop
“Clone” downloads your GitHub repo into a folder on your laptop that stays connected to GitHub.
- In the VS Code terminal, go to your Documents folder and clone. Paste your URL from Step 0.6 (right-click in the terminal to paste).
You should seeTerminal
cd ~/Documents git clone https://github.com/YOUR-USERNAME/cinesuper-YOUR-JSOFT-ID.gitCloning into 'cinesuper-…'… done. - Open the folder in VS Code: File → Open Folder… → Documents → cinesuper-<jsoft-id> → Select Folder / Open. If asked “Do you trust the authors?”, click Yes, I trust the authors.
- Open a fresh terminal (Terminal → New Terminal). It now starts inside your repo. Check it:
It should sayTerminal
git statusOn branch mainandYour branch is up to date with 'origin/main'.
cinesuper-<jsoft-id>. Every Git command is run from a terminal opened in this folder. If Git says not a git repository, you are in the wrong folder.Step 0.8 — How to create files and folders in VS Code
- In the Explorer panel (left, top icon) hover over your folder name and click the New File icon (page with +).
- Type the file name and press Enter. Typing a
/creates a folder too:database/01_tables.sqlcreates thedatabasefolder and the file inside it. - Paste the content and press
Ctrl + S(Mac:Cmd + S) to save. An unsaved file shows a ● on its tab, and Git cannot see unsaved changes. - To add screenshots, save or drag the image files into the
screenshotsfolder in the Explorer.
By the end of the project your folder will look like this:
cinesuper-<jsoft-id>/
├── README.md
├── index.html
├── style.css
├── config.js
├── app.js
├── database/
│ ├── 01_tables.sql
│ ├── 02_seed.sql
│ ├── 03_security.sql
│ ├── 04_view.sql
│ ├── 05_queries.sql
│ └── 06_personalisation.sql
└── screenshots/Step 0.9 — The push routine (use it at every checkpoint)
Every PUSH box in this guide means: save your files, then run these three commands in the VS Code terminal. Only the commit message changes, and each PUSH box gives you its exact message. For example, at PUSH 2 you run:
git add .
# Use the exact message shown in each PUSH box
git commit -m "Phase 3: create tables"
git pushPhase 0: repo setup, Phase 3: create tables … Phase 12: personalisation). They are checked automatically, and a missing or wrong message counts as a missing checkpoint.| Command | What it does |
|---|---|
git status | Shows changed and new files (red = not added yet, green = added) |
git add . | Puts all changed files in this folder into the next commit (the dot means “everything here”) |
git commit -m "…" | Saves a snapshot on your laptop with a message |
git push | Sends your commits to GitHub |
git log --oneline | Lists your commits (press q to exit) |
git push, VS Code or Git asks you to sign in to GitHub in the browser. Click Allow / Authorize. After that it remembers you.In VS Code create the empty file screenshots/.gitkeep (Git does not save empty folders, so this keeps the folder) and save it. Then run:
git add .
git commit -m "Phase 0: repo setup"
git pushRefresh your repo on GitHub: you should see the screenshots folder.
Create your Supabase project
- On the Supabase dashboard click New project. If asked, create an Organization with your name on the Free plan.
- Fill in:
- Project name:
cinesuper-<your-jsoft-id> - Database password: Generate a password, copy and save it in a notepad file
- Region: the one closest to India (Mumbai / South Asia if listed)
- Project name:
- Click Create new project and wait 1 to 2 minutes.
Understand the database design
Three tables, two foreign keys: one genre has many movies, and one movie has many reviews.
| Table | Stores | Example row |
|---|---|---|
genres | Movie categories | Action |
movies | Movie details | Drishyam, 2013, Malayalam, 160 min |
reviews | Ratings and comments per movie | Anjali gave Drishyam 5 stars |
ER diagram
erDiagram
GENRES ||--o{ MOVIES : "categorises"
MOVIES ||--o{ REVIEWS : "receives"
GENRES {
bigint id PK
text name "UNIQUE NOT NULL"
}
MOVIES {
bigint id PK
text title "NOT NULL"
int release_year "CHECK 1900-2100"
text language "NOT NULL"
int duration_min "CHECK > 0"
text description
text poster_url
bigint genre_id FK
timestamptz created_at
}
REVIEWS {
bigint id PK
bigint movie_id FK
text reviewer_name "NOT NULL"
int rating "CHECK 1-5"
text comment
timestamptz created_at
}Create the tables
- In Supabase, open SQL Editor from the left sidebar.
- Click New query.
- Paste Script 1 and click Run (or
Ctrl + Enter).
| Column | Type | Rules | Meaning |
|---|---|---|---|
id | bigint | Primary key, auto-number | Unique number for each genre |
name | text | Not null, unique | Genre name, e.g. Action |
| Column | Type | Rules | Meaning |
|---|---|---|---|
id | bigint | Primary key, auto-number | Unique number for each movie |
title | text | Not null | Movie name |
release_year | int | Between 1900 and 2100 | Year of release |
language | text | Not null | Malayalam, Tamil, English … |
duration_min | int | Greater than 0 | Running time in minutes |
description | text | Optional | Short story summary |
poster_url | text | Optional | Link to the poster image |
genre_id | bigint | Foreign key → genres.id, set null on delete | Which genre the movie belongs to |
created_at | timestamptz | Default now() | When the row was added |
| Column | Type | Rules | Meaning |
|---|---|---|---|
id | bigint | Primary key, auto-number | Unique number for each review |
movie_id | bigint | Not null, foreign key → movies.id, cascade on delete | Which movie is reviewed |
reviewer_name | text | Not null | Who wrote the review |
rating | int | Not null, between 1 and 5 | Star rating |
comment | text | Optional | Review text |
created_at | timestamptz | Default now() | When the review was written |
One genre has many movies (genres.id → movies.genre_id). One movie has many reviews (movies.id → reviews.movie_id).
-- SCRIPT 1: CREATE TABLES
create table genres (
id bigint generated always as identity primary key,
name text not null unique
);
create table movies (
id bigint generated always as identity primary key,
title text not null,
release_year int check (release_year between 1900 and 2100),
language text not null,
duration_min int check (duration_min > 0),
description text,
poster_url text,
genre_id bigint references genres(id) on delete set null,
created_at timestamptz default now()
);
create table reviews (
id bigint generated always as identity primary key,
movie_id bigint not null references movies(id) on delete cascade,
reviewer_name text not null,
rating int not null check (rating between 1 and 5),
comment text,
created_at timestamptz default now()
);What each keyword means
| Keyword | Meaning |
|---|---|
generated always as identity | Auto-increment id (1, 2, 3 …) |
primary key | Unique identifier of each row |
not null | Value is compulsory |
unique | No two rows share the value |
check (…) | Only values meeting the condition are allowed |
references genres(id) | Foreign key to an existing genre |
on delete set null | Deleting a genre leaves its movies with no genre |
on delete cascade | Deleting a movie deletes its reviews |
In VS Code create database/01_tables.sql, paste Script 1 and save. Then run the push routine:
git add .
git commit -m "Phase 3: create tables"
git pushInsert sample data
Open a new query, paste Script 2 and run it once.
| id | name |
|---|---|
| 1 | Action |
| 2 | Drama |
| 3 | Comedy |
| 4 | Sci-Fi |
| 5 | Thriller |
| 6 | Biography |
| id | title | release_year | language | duration_min | genre_id | description |
|---|---|---|---|---|---|---|
| 1 | Drishyam | 2013 | Malayalam | 160 | 5 (Thriller) | A cable operator goes to any length to protect his family after an accidental crime. |
| 2 | Premam | 2015 | Malayalam | 156 | 2 (Drama) | George falls in love three times, from school days to adulthood. |
| 3 | Bangalore Days | 2014 | Malayalam | 171 | 3 (Comedy) | Three cousins from Kerala chase their dreams in Bangalore. |
| 4 | Minnal Murali | 2021 | Malayalam | 158 | 1 (Action) | A village tailor gets superpowers after being struck by lightning. |
| 5 | Manjummel Boys | 2024 | Malayalam | 135 | 5 (Thriller) | Friends from Kochi race to rescue one of them trapped in the Guna Caves. |
| 6 | Vikram | 2022 | Tamil | 174 | 1 (Action) | A special agent investigates masked murders linked to a drug cartel. |
| 7 | 96 | 2018 | Tamil | 158 | 2 (Drama) | Two school sweethearts meet again at a reunion, twenty-two years later. |
| 8 | Jai Bhim | 2021 | Tamil | 164 | 2 (Drama) | A lawyer fights for justice for a tribal woman whose husband goes missing in custody. |
| 9 | Memento | 2000 | English | 113 | 5 (Thriller) | A man with short-term memory loss hunts his wife's killer using notes and tattoos. |
| 10 | The Dark Knight | 2008 | English | 152 | 1 (Action) | Batman faces the Joker, a criminal who wants to plunge Gotham into chaos. |
| 11 | Inception | 2010 | English | 148 | 4 (Sci-Fi) | A thief who steals secrets through dreams is asked to plant an idea instead. |
| 12 | Interstellar | 2014 | English | 169 | 4 (Sci-Fi) | Astronauts travel through a wormhole to find a new home for humanity. |
| 13 | Oppenheimer | 2023 | English | 180 | 6 (Biography) | The story of J. Robert Oppenheimer and the creation of the atomic bomb. |
| id | movie_id | reviewer_name | rating | comment |
|---|---|---|---|---|
| 1 | 1 (Drishyam) | Anjali | 5 ★★★★★ | Georgekutty is a genius! |
| 2 | 1 (Drishyam) | Arjun | 5 ★★★★★ | Best thriller ever. |
| 3 | 2 (Premam) | Sreya | 4 ★★★★ | Malar teacher steals the show. |
| 4 | 5 (Manjummel Boys) | Akhil | 5 ★★★★★ | Gripping survival story. |
| 5 | 6 (Vikram) | Fathima | 4 ★★★★ | Mass climax! |
| 6 | 12 (Interstellar) | Nikhil | 5 ★★★★★ | Mind-blowing science. |
| 7 | 11 (Inception) | Arjun | 4 ★★★★ | Need to watch it twice. |
| 8 | 10 (The Dark Knight) | Akhil | 5 ★★★★★ | The Joker is legendary. |
poster_url and created_at are not shown. The id numbers match if you ran each script once; they can differ if you re-ran a script.
-- SCRIPT 2: SAMPLE DATA (Malayalam, Tamil and Christopher Nolan movies)
insert into genres (name) values
('Action'), ('Drama'), ('Comedy'), ('Sci-Fi'), ('Thriller'), ('Biography');
insert into movies (title, release_year, language, duration_min, description, poster_url, genre_id) values
-- Malayalam
('Drishyam', 2013, 'Malayalam', 160,
'A cable operator goes to any length to protect his family after an accidental crime.',
'https://placehold.co/300x450/7f1d1d/ffffff?text=Drishyam',
(select id from genres where name = 'Thriller')),
('Premam', 2015, 'Malayalam', 156,
'George falls in love three times, from school days to adulthood.',
'https://placehold.co/300x450/831843/ffffff?text=Premam',
(select id from genres where name = 'Drama')),
('Bangalore Days', 2014, 'Malayalam', 171,
'Three cousins from Kerala chase their dreams in Bangalore.',
'https://placehold.co/300x450/065f46/ffffff?text=Bangalore+Days',
(select id from genres where name = 'Comedy')),
('Minnal Murali', 2021, 'Malayalam', 158,
'A village tailor gets superpowers after being struck by lightning.',
'https://placehold.co/300x450/1e3a8a/ffffff?text=Minnal+Murali',
(select id from genres where name = 'Action')),
('Manjummel Boys', 2024, 'Malayalam', 135,
'Friends from Kochi race to rescue one of them trapped in the Guna Caves.',
'https://placehold.co/300x450/422006/ffffff?text=Manjummel+Boys',
(select id from genres where name = 'Thriller')),
-- Tamil
('Vikram', 2022, 'Tamil', 174,
'A special agent investigates masked murders linked to a drug cartel.',
'https://placehold.co/300x450/111827/ffffff?text=Vikram',
(select id from genres where name = 'Action')),
('96', 2018, 'Tamil', 158,
'Two school sweethearts meet again at a reunion, twenty-two years later.',
'https://placehold.co/300x450/9d174d/ffffff?text=96',
(select id from genres where name = 'Drama')),
('Jai Bhim', 2021, 'Tamil', 164,
'A lawyer fights for justice for a tribal woman whose husband goes missing in custody.',
'https://placehold.co/300x450/1e40af/ffffff?text=Jai+Bhim',
(select id from genres where name = 'Drama')),
-- Christopher Nolan
('Memento', 2000, 'English', 113,
'A man with short-term memory loss hunts his wife''s killer using notes and tattoos.',
'https://placehold.co/300x450/374151/ffffff?text=Memento',
(select id from genres where name = 'Thriller')),
('The Dark Knight', 2008, 'English', 152,
'Batman faces the Joker, a criminal who wants to plunge Gotham into chaos.',
'https://placehold.co/300x450/0f172a/ffffff?text=The+Dark+Knight',
(select id from genres where name = 'Action')),
('Inception', 2010, 'English', 148,
'A thief who steals secrets through dreams is asked to plant an idea instead.',
'https://placehold.co/300x450/334155/ffffff?text=Inception',
(select id from genres where name = 'Sci-Fi')),
('Interstellar', 2014, 'English', 169,
'Astronauts travel through a wormhole to find a new home for humanity.',
'https://placehold.co/300x450/0c4a6e/ffffff?text=Interstellar',
(select id from genres where name = 'Sci-Fi')),
('Oppenheimer', 2023, 'English', 180,
'The story of J. Robert Oppenheimer and the creation of the atomic bomb.',
'https://placehold.co/300x450/7c2d12/ffffff?text=Oppenheimer',
(select id from genres where name = 'Biography'));
insert into reviews (movie_id, reviewer_name, rating, comment) values
((select id from movies where title = 'Drishyam'), 'Anjali', 5, 'Georgekutty is a genius!'),
((select id from movies where title = 'Drishyam'), 'Arjun', 5, 'Best thriller ever.'),
((select id from movies where title = 'Premam'), 'Sreya', 4, 'Malar teacher steals the show.'),
((select id from movies where title = 'Manjummel Boys'), 'Akhil', 5, 'Gripping survival story.'),
((select id from movies where title = 'Vikram'), 'Fathima', 4, 'Mass climax!'),
((select id from movies where title = 'Interstellar'), 'Nikhil', 5, 'Mind-blowing science.'),
((select id from movies where title = 'Inception'), 'Arjun', 4, 'Need to watch it twice.'),
((select id from movies where title = 'The Dark Knight'), 'Akhil', 5, 'The Joker is legendary.');(select id from genres where name = 'Action') looks up IDs by name instead of hard-coding them.Create database/02_seed.sql with Script 2 and save. Then push:
git add .
git commit -m "Phase 4: sample data"
git pushSecurity: RLS and permissions
The website talks to the database from the browser with a public key. These rules let everyone read and add valid reviews, and stop anyone from updating or deleting.
| Policy | Table | Action | Who | Rule |
|---|---|---|---|---|
| Public can read genres | genres | SELECT | anon, authenticated | Everyone can read every row |
| Public can read movies | movies | SELECT | anon, authenticated | Everyone can read every row |
| Public can read reviews | reviews | SELECT | anon, authenticated | Everyone can read every row |
| Public can add reviews | reviews | INSERT | anon, authenticated | Rating 1–5, name 1–50 characters, comment up to 500 characters |
| Table | anon / authenticated can | Cannot |
|---|---|---|
genres | SELECT | INSERT, UPDATE, DELETE |
movies | SELECT | INSERT, UPDATE, DELETE |
reviews | SELECT, INSERT | UPDATE, DELETE |
RLS is switched on for all three tables. Anything without a policy is blocked.
-- SCRIPT 3: SECURITY (RLS + GRANTS)
alter table genres enable row level security;
alter table movies enable row level security;
alter table reviews enable row level security;
create policy "Public can read genres"
on genres for select to anon, authenticated using (true);
create policy "Public can read movies"
on movies for select to anon, authenticated using (true);
create policy "Public can read reviews"
on reviews for select to anon, authenticated using (true);
create policy "Public can add reviews"
on reviews for insert to anon, authenticated
with check (
rating between 1 and 5
and length(trim(reviewer_name)) between 1 and 50
and length(coalesce(comment, '')) <= 500
);
grant usage on schema public to anon, authenticated;
grant select on genres, movies, reviews to anon, authenticated;
grant insert on reviews to anon, authenticated;Create database/03_security.sql with Script 3, save it, and put a screenshot of the policies page in screenshots/. Then push:
git add .
git commit -m "Phase 5: RLS policies"
git pushView and practice queries
Step 6.1 — Average-rating view
| id | title | avg_rating | review_count |
|---|---|---|---|
| 1 | Drishyam | 5.0 | 2 |
| 5 | Manjummel Boys | 5.0 | 1 |
| 10 | The Dark Knight | 5.0 | 1 |
| 12 | Interstellar | 5.0 | 1 |
| 2 | Premam | 4.0 | 1 |
| 6 | Vikram | 4.0 | 1 |
| 11 | Inception | 4.0 | 1 |
| 3 | Bangalore Days | NULL | 0 |
| 4 | Minnal Murali | NULL | 0 |
| 7 | 96 | NULL | 0 |
| 8 | Jai Bhim | NULL | 0 |
| 9 | Memento | NULL | 0 |
| 13 | Oppenheimer | NULL | 0 |
Movies with no reviews still appear (LEFT JOIN) with avg_rating = NULL and review_count = 0. Rows with the same rating may appear in a different order.
-- SCRIPT 4: VIEW FOR AVERAGE RATINGS
create view movie_ratings
with (security_invoker = on) as
select
m.id,
m.title,
round(avg(r.rating), 1) as avg_rating,
count(r.id) as review_count
from movies m
left join reviews r on r.movie_id = m.id
group by m.id, m.title;
grant select on movie_ratings to anon, authenticated;
-- test it
select * from movie_ratings order by avg_rating desc nulls last;security_invoker = on makes the view obey the RLS rules of whoever queries it. LEFT JOIN keeps movies with zero reviews.
Step 6.2 — Part A: run these and screenshot each output
| title | release_year | genre |
|---|---|---|
| Drishyam | 2013 | Thriller |
| Premam | 2015 | Drama |
| Bangalore Days | 2014 | Comedy |
| Minnal Murali | 2021 | Action |
| Manjummel Boys | 2024 | Thriller |
| Vikram | 2022 | Action |
| 96 | 2018 | Drama |
| Jai Bhim | 2021 | Drama |
| Memento | 2000 | Thriller |
| The Dark Knight | 2008 | Action |
| Inception | 2010 | Sci-Fi |
| Interstellar | 2014 | Sci-Fi |
| Oppenheimer | 2023 | Biography |
| name | total_movies |
|---|---|
| Action | 3 |
| Drama | 3 |
| Thriller | 3 |
| Sci-Fi | 2 |
| Comedy | 1 |
| Biography | 1 |
| title | duration_min |
|---|---|
| Drishyam | 160 |
| Premam | 156 |
| Bangalore Days | 171 |
| Minnal Murali | 158 |
| Vikram | 174 |
| 96 | 158 |
| Jai Bhim | 164 |
| The Dark Knight | 152 |
| Interstellar | 169 |
| Oppenheimer | 180 |
| title | release_year |
|---|---|
| Minnal Murali | 2021 |
| Manjummel Boys | 2024 |
| title | avg_rating |
|---|---|
| Drishyam | 5.0 |
| Manjummel Boys | 5.0 |
| The Dark Knight | 5.0 |
| Interstellar | 5.0 |
| title |
|---|
| Bangalore Days |
| Minnal Murali |
| 96 |
| Jai Bhim |
| Memento |
| Oppenheimer |
| title | duration_min |
|---|---|
| Oppenheimer | 180 |
| result |
|---|
UPDATE 1 — Interstellar's description is changed. Run select title, description from movies where title = 'Interstellar'; to see it. |
Row order can differ for queries without ORDER BY, and for ties. The values must match.
-- Q1. Movies with genre name (INNER JOIN)
select m.title, m.release_year, g.name as genre
from movies m join genres g on g.id = m.genre_id;
-- Q2. Movies per genre (GROUP BY)
select g.name, count(m.id) as total_movies
from genres g left join movies m on m.genre_id = g.id
group by g.name order by total_movies desc;
-- Q3. Movies longer than 150 minutes
select title, duration_min from movies where duration_min > 150;
-- Q4. Malayalam movies after 2015
select title, release_year from movies
where language = 'Malayalam' and release_year > 2015;
-- Q5. Average rating at least 4.5 (HAVING)
select m.title, round(avg(r.rating),1) as avg_rating
from movies m join reviews r on r.movie_id = m.id
group by m.title having avg(r.rating) >= 4.5;
-- Q6. Movies with no reviews (subquery)
select title from movies
where id not in (select movie_id from reviews);
-- Q7. Longest movie
select title, duration_min from movies
where duration_min = (select max(duration_min) from movies);
-- Q8. Update a description
update movies set description = 'An epic sci-fi journey through space and time.'
where title = 'Interstellar';Step 6.3 — Part B: write your own (mandatory)
- All reviews with the movie title (JOIN).
- Number of movies per language.
- The newest movie in the database.
- Movies whose title starts with M (
LIKE). - Delete one review; screenshot the table before and after.
Create database/04_view.sql (Script 4) and database/05_queries.sql (Q1–Q8 + your Part B), and put all query screenshots in screenshots/. Then push:
git add .
git commit -m "Phase 6: view and queries"
git pushGet your API keys
- In Supabase, click Connect at the top of the project dashboard, or open Project Settings → API Keys.
- Copy the Project URL, like
https://abcdxyz.supabase.co. - Copy the Publishable key (older projects call it the anon public key).
- Save both in your notepad file.
Build the frontend
In VS Code, with your cinesuper-<jsoft-id> repo folder open, create these four files at the top level of the folder (next to README.md). Names are case-sensitive.
Step 8.1 — config.js (your own keys)
// Replace both values with YOUR Supabase details from Phase 7
const SUPABASE_URL = "https://YOUR-PROJECT-ID.supabase.co";
const SUPABASE_KEY = "YOUR-PUBLISHABLE-OR-ANON-KEY";Step 8.2 — index.html (edit the footer with your name and JSOFT ID)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CineSuper - Mini OTT</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>🎬 CineSuper</h1>
<p>Your mini OTT movie library</p>
</header>
<section class="controls">
<input id="search" type="text" placeholder="🔍 Search movies...">
<select id="genreFilter">
<option value="">All Genres</option>
</select>
</section>
<p id="status">Loading movies...</p>
<main id="movieGrid" class="grid"></main>
<!-- Movie detail popup -->
<div id="modal" class="modal hidden">
<div class="modal-content">
<button id="closeModal" class="close">×</button>
<div id="movieDetail"></div>
<h3>Reviews</h3>
<div id="reviewList"></div>
<h3>Add your review</h3>
<form id="reviewForm">
<input id="reviewerName" type="text" placeholder="Your name" maxlength="50" required>
<select id="reviewRating" required>
<option value="">Select rating</option>
<option value="5">⭐⭐⭐⭐⭐ (5)</option>
<option value="4">⭐⭐⭐⭐ (4)</option>
<option value="3">⭐⭐⭐ (3)</option>
<option value="2">⭐⭐ (2)</option>
<option value="1">⭐ (1)</option>
</select>
<textarea id="reviewComment" placeholder="Write your comment" maxlength="500" required></textarea>
<button type="submit">Submit Review</button>
</form>
</div>
</div>
<footer>
<p>Built by <strong>YOUR NAME</strong> · JSOFT ID: <strong>YOUR JSOFT ID</strong></p>
<p>Jain School of Future Technology · Faculty: Sathish Kumar M</p>
<p class="powered">Powered by Supabase</p>
</footer>
<!-- Order matters: 1) Supabase library 2) your keys 3) your app -->
<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
<script src="config.js"></script>
<script src="app.js"></script>
</body>
</html>Step 8.3 — style.css
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: "Segoe UI", Arial, sans-serif;
background: #0f0f14;
color: #f1f1f1;
min-height: 100vh;
}
header { text-align: center; padding: 30px 16px 10px; }
header h1 { font-size: 2.4rem; color: #e50914; }
header p { color: #aaa; margin-top: 4px; }
.controls {
display: flex; gap: 12px; justify-content: center;
flex-wrap: wrap; padding: 16px;
}
.controls input, .controls select {
padding: 10px 14px; border-radius: 8px;
border: 1px solid #333; background: #1c1c24; color: #fff;
font-size: 1rem; min-width: 220px;
}
#status { text-align: center; color: #888; margin: 6px 0 12px; }
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 18px; padding: 0 24px 40px;
max-width: 1200px; margin: 0 auto;
}
.card {
background: #1c1c24; border-radius: 10px; overflow: hidden;
cursor: pointer; transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover { transform: scale(1.04); box-shadow: 0 8px 20px rgba(229, 9, 20, 0.35); }
.card img { width: 100%; aspect-ratio: 2 / 3; object-fit: cover; display: block; }
.card-body { padding: 10px; }
.card-body h3 { font-size: 1rem; margin-bottom: 4px; }
.card-body p { font-size: 0.8rem; color: #aaa; }
.stars { color: #fbbf24 !important; margin-top: 4px; }
.modal {
position: fixed; inset: 0; background: rgba(0, 0, 0, 0.8);
display: flex; align-items: center; justify-content: center;
padding: 16px; z-index: 10;
}
.hidden { display: none; }
.modal-content {
background: #1c1c24; border-radius: 12px;
max-width: 600px; width: 100%; max-height: 90vh; overflow-y: auto;
padding: 24px; position: relative;
}
.close {
position: absolute; top: 10px; right: 14px;
background: none; border: none; color: #fff; font-size: 1.8rem; cursor: pointer;
}
.modal-content h2 { color: #e50914; margin-bottom: 6px; }
.modal-content h3 { margin: 20px 0 10px; }
.meta { color: #aaa; font-size: 0.9rem; margin-bottom: 10px; }
.review { background: #26262f; padding: 10px 12px; border-radius: 8px; margin-bottom: 8px; }
.review strong { color: #fbbf24; }
.review small { color: #777; display: block; margin-top: 4px; }
#reviewForm { display: flex; flex-direction: column; gap: 10px; }
#reviewForm input, #reviewForm select, #reviewForm textarea {
padding: 10px; border-radius: 8px;
border: 1px solid #333; background: #0f0f14; color: #fff; font-size: 0.95rem;
}
#reviewForm textarea { min-height: 70px; resize: vertical; }
#reviewForm button {
padding: 12px; border: none; border-radius: 8px;
background: #e50914; color: #fff; font-weight: bold; cursor: pointer;
}
#reviewForm button:hover { background: #b20710; }
footer {
text-align: center; padding: 20px; line-height: 1.7;
color: #888; font-size: 0.85rem; border-top: 1px solid #222;
}
footer strong { color: #e5e5e5; }
footer .powered { color: #555; font-size: 0.75rem; }Step 8.4 — app.js (connects the page to Supabase)
// 1. Connect to Supabase
const db = supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
// 2. Page elements
const grid = document.getElementById("movieGrid");
const statusEl = document.getElementById("status");
const searchInput = document.getElementById("search");
const genreFilter = document.getElementById("genreFilter");
const modal = document.getElementById("modal");
const movieDetail = document.getElementById("movieDetail");
const reviewList = document.getElementById("reviewList");
const reviewForm = document.getElementById("reviewForm");
let movies = [];
let ratingsMap = {};
let currentMovieId = null;
// Stops users injecting HTML/JS through reviews (safe in text and attributes)
function escapeHtml(text) {
return String(text ?? "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function showError(error) {
console.error(error);
statusEl.textContent = "❌ Error: " + error.message;
}
// 3. SELECT id, name FROM genres ORDER BY name
async function loadGenres() {
const { data, error } = await db.from("genres").select("id, name").order("name");
if (error) return showError(error);
data.forEach((g) => {
const opt = document.createElement("option");
opt.value = g.id;
opt.textContent = g.name;
genreFilter.appendChild(opt);
});
}
// 4. SELECT * FROM movie_ratings (the view)
async function loadRatings() {
const { data, error } = await db.from("movie_ratings").select("id, avg_rating, review_count");
if (error) return console.error(error);
ratingsMap = {};
data.forEach((r) => (ratingsMap[r.id] = r));
}
// 5. SELECT movies JOIN genres, with search and filter
async function loadMovies() {
statusEl.textContent = "Loading movies...";
let query = db
.from("movies")
.select("id, title, release_year, language, duration_min, description, poster_url, genres(name)")
.order("release_year", { ascending: false });
const search = searchInput.value.trim();
const genreId = genreFilter.value;
if (search) query = query.ilike("title", `%${search}%`);
if (genreId) query = query.eq("genre_id", genreId);
const { data, error } = await query;
if (error) return showError(error);
movies = data;
statusEl.textContent = data.length ? `${data.length} movie(s) found` : "No movies found 😕";
grid.innerHTML = data.map(movieCard).join("");
document.querySelectorAll(".card").forEach((card) =>
card.addEventListener("click", () => openMovie(Number(card.dataset.id)))
);
}
function movieCard(m) {
const r = ratingsMap[m.id];
const stars = r && r.avg_rating ? `⭐ ${r.avg_rating} (${r.review_count})` : "No ratings yet";
return `
<div class="card" data-id="${m.id}">
<img src="${escapeHtml(m.poster_url)}" alt="${escapeHtml(m.title)}">
<div class="card-body">
<h3>${escapeHtml(m.title)}</h3>
<p>${m.release_year} · ${escapeHtml(m.language)} · ${escapeHtml(m.genres?.name)}</p>
<p class="stars">${stars}</p>
</div>
</div>`;
}
// 6. Movie detail popup
async function openMovie(id) {
const m = movies.find((x) => x.id === id);
if (!m) return;
currentMovieId = id;
movieDetail.innerHTML = `
<h2>${escapeHtml(m.title)}</h2>
<p class="meta">${m.release_year} · ${escapeHtml(m.language)} · ${escapeHtml(m.genres?.name)} · ${m.duration_min} min</p>
<p>${escapeHtml(m.description)}</p>`;
modal.classList.remove("hidden");
await loadReviews(id);
}
// 7. SELECT * FROM reviews WHERE movie_id = ?
async function loadReviews(movieId) {
reviewList.innerHTML = "<p>Loading reviews...</p>";
const { data, error } = await db
.from("reviews")
.select("reviewer_name, rating, comment, created_at")
.eq("movie_id", movieId)
.order("created_at", { ascending: false });
if (error) {
reviewList.innerHTML = `<p>Error: ${escapeHtml(error.message)}</p>`;
return;
}
reviewList.innerHTML = data.length
? data.map((r) => `
<div class="review">
<strong>${"⭐".repeat(r.rating)}</strong> — ${escapeHtml(r.reviewer_name)}
<p>${escapeHtml(r.comment)}</p>
<small>${new Date(r.created_at).toLocaleString()}</small>
</div>`).join("")
: "<p>No reviews yet. Be the first!</p>";
}
// 8. INSERT INTO reviews
reviewForm.addEventListener("submit", async (e) => {
e.preventDefault();
const newReview = {
movie_id: currentMovieId,
reviewer_name: document.getElementById("reviewerName").value.trim(),
rating: Number(document.getElementById("reviewRating").value),
comment: document.getElementById("reviewComment").value.trim(),
};
const { error } = await db.from("reviews").insert(newReview);
if (error) {
alert("Could not save review: " + error.message);
return;
}
reviewForm.reset();
await loadReviews(currentMovieId);
await loadRatings();
await loadMovies();
});
// 9. Close popup
document.getElementById("closeModal").addEventListener("click", () => modal.classList.add("hidden"));
modal.addEventListener("click", (e) => { if (e.target === modal) modal.classList.add("hidden"); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") modal.classList.add("hidden"); });
// 10. Search (waits until typing stops) and genre filter
let typingTimer;
searchInput.addEventListener("input", () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(loadMovies, 300);
});
genreFilter.addEventListener("change", loadMovies);
// 11. Start
async function init() {
await loadGenres();
await loadRatings();
await loadMovies();
}
init();JavaScript to SQL (learn this for the viva)
| Supabase JavaScript | Equivalent SQL |
|---|---|
db.from("genres").select("id, name") | SELECT id, name FROM genres |
.order("name") | ORDER BY name |
.select("…, genres(name)") | JOIN genres ON genres.id = movies.genre_id |
.ilike("title", "%drish%") | WHERE title ILIKE '%drish%' |
.eq("genre_id", 3) | WHERE genre_id = 3 |
db.from("reviews").insert({…}) | INSERT INTO reviews (…) VALUES (…) |
Test on your laptop
Test the Phase 8 files before you push them. Double-click index.html to open it in Chrome, or in VS Code install Live Server → right-click index.html → Open with Live Server.
- 13 movies appear with posters
- Genre dropdown lists 6 genres
- Typing
inshows Inception, Interstellar and Minnal Murali (search ignores case) - Clicking a movie opens the popup with its reviews
- A submitted review appears immediately in the popup
- The same review appears in Supabase → Table Editor → reviews
Check that index.html, style.css, config.js and app.js are at the top level of your repo folder (next to README.md, not inside database/). Then push:
git add .
git commit -m "Phase 8: frontend"
git pushDeploy with GitHub Pages
- In your repo, open Settings → Pages (left sidebar).
- Under Build and deployment → Source, choose Deploy from a branch.
- Branch: main, folder: / (root) → Save.
- Wait 1 to 3 minutes and refresh. GitHub shows your live link:
https://YOUR-USERNAME.github.io/cinesuper-YOUR-JSOFT-ID/ - Open the link on your phone and submit one review from the phone.
README with your live link
Open README.md in VS Code, replace everything with this, fill in your details and save.
# 🎬 CineSuper — Mini OTT Movie Database
**Live Demo:** https://YOUR-USERNAME.github.io/cinesuper-YOUR-JSOFT-ID/
**Student:** YOUR NAME | **JSOFT ID:** YOUR JSOFT ID
**Institution:** Jain School of Future Technology
**Course:** Database Management Systems | **Faculty:** Sathish Kumar M
## Tech Stack
- Supabase (PostgreSQL) – database
- HTML, CSS, JavaScript – frontend
- GitHub Pages – hosting
## Database
- genres (id, name)
- movies (id, title, release_year, language, duration_min, description, poster_url, genre_id → genres)
- reviews (id, movie_id → movies, reviewer_name, rating 1–5, comment, created_at)
- View: movie_ratings (average rating per movie)
- SQL scripts: see the `database/` folder
## Features
- Browse, search and filter movies
- Movie details with reviews
- Add a review (saved to the database)
- Row Level Security enabled
## My Personalisation
- New movies added: …
- New column: …
- Extra feature: …Save README.md, then push:
git add .
git commit -m "Phase 11: README and live link"
git pushPersonalise your project
Every repo must be different. Unchanged copies get zero for this part.
Add 5 movies
Your own INSERT statements, with at least one new genre (Horror, Romance or Documentary).
Add a column
In the database and visible on the site.
Add a feature
Pick one from the table below.
New theme colour
Replace the #e50914 red in style.css.
12.2 — Column options
| Column | Show it |
|---|---|
imdb_rating numeric(3,1) | On the card |
director text | In the popup |
is_trending boolean default false | A 🔥 badge on trending cards |
age_rating text check (age_rating in ('U','UA','A')) | On the card |
-- example
alter table movies add column director text;
update movies set director = 'Jeethu Joseph' where title = 'Drishyam';
update movies set director = 'Christopher Nolan'
where title in ('Memento', 'The Dark Knight', 'Inception', 'Interstellar', 'Oppenheimer');Then add the column to the .select(...) in loadMovies() and display it in movieCard() or openMovie().
12.3 — Feature options
| Level | Feature |
|---|---|
| Easy | Language filter dropdown |
| Easy | Sort by Newest / Oldest / Top Rated |
| Medium | Top 5 Rated strip at the top (from the view) |
| Medium | watchlist table + Add to Watchlist button |
| Hard | Supabase Auth: only logged-in users can review |
| Hard | Real posters uploaded to Supabase Storage |
alter table watchlist enable row level security;, add select and insert policies, and grant select, insert on watchlist to anon, authenticated;, just like Script 3 did for reviews. Without them you get permission denied or violates row-level security policy."Public can add reviews" policy and recreate it with to authenticated instead of to anon, authenticated.Create database/06_personalisation.sql with your INSERT, ALTER and UPDATE statements, save your changed frontend files, add screenshots of the new features, and fill the README's My Personalisation section. Then push:
git add .
git commit -m "Phase 12: personalisation"
git pushPush verification
A project counts as complete only when all 8 checkpoint commits are visible in the repo's history and the live link works. Open your repo → click commits above the file list to see it.
| # | Commit message | Must contain |
|---|---|---|
| 1 | Phase 0: repo setup | README.md, screenshots/ |
| 2 | Phase 3: create tables | database/01_tables.sql |
| 3 | Phase 4: sample data | database/02_seed.sql |
| 4 | Phase 5: RLS policies | database/03_security.sql + policies screenshot |
| 5 | Phase 6: view and queries | 04_view.sql, 05_queries.sql + query screenshots |
| 6 | Phase 8: frontend | index.html, style.css, config.js, app.js |
| 7 | Phase 11: README and live link | README with live link, name, JSOFT ID |
| 8 | Phase 12: personalisation | 06_personalisation.sql + updated frontend + screenshots |
Deadlines
End of Saturday
Commits 1–5Database, security and queries pushed. Commit dates are visible in your history.
Sunday afternoon
Commits 6–7Frontend pushed and the live link works on your phone.
Final deadline
Commit 8 · Sunday nightAll 8 commits by the deadline your faculty announces. Commits after it are not evaluated.
How faculty verifies in the next class (2 minutes per student)
- Open the repo's commit history: 8 phase commits present.
- Open the live link, add a test review, confirm it appears.
- Student shows that review in their own Supabase Table Editor.
- Footer shows name, JSOFT ID and Jain School of Future Technology.
- The Phase 12 column and feature are visible on the site.
Submission checklist
- Live GitHub Pages link
- GitHub repo link (
cinesuper-<jsoft-id>, public) - All 8 checkpoint commits in the history
- All SQL scripts in
database/ - Screenshots in
screenshots/: tables, RLS policies, Q1–Q8, Part B, new movies, new column, extra feature - ER diagram including your new column, uploaded to
screenshots/ - README filled with name, JSOFT ID, live link and personalisation
Submit your project
When everything above is ready, submit your repo link and live link in the submission form. Submit only once. The form accepts one submission per JSOFT ID. After you submit, fixes you push to GitHub are picked up automatically, so you never need to submit again. If your repo or live link changes, tell your faculty.
Rubric · 25 marks
| Component | Marks |
|---|---|
| Schema: tables, PK, FK, constraints | 4 |
| Sample data + own 5 movies | 3 |
| RLS policies + grants | 3 |
| SQL queries Part A + Part B with outputs | 5 |
| Live site works: browse, search, filter, review | 4 |
| Personalisation: new column + extra feature | 4 |
| All 8 commits, README, ER diagram | 2 |
| Total | 25 |
A short viva will be held in the class after the weekend; marks can be reduced if you cannot explain your own code.
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| Stuck on “Loading movies...” | JavaScript error | F12 → Console, read the red error |
supabase is not defined | Script order wrong or offline | Supabase CDN script must come before config.js and app.js |
SUPABASE_URL is not defined | config.js missing or misnamed | File must be exactly config.js, at the repo root |
| Invalid API key / 401 | Wrong key | Re-copy the publishable/anon key, no extra spaces or quotes |
| Failed to fetch | Wrong URL or paused project | Check the URL; restore the project in Supabase |
| “0 movies found” but data exists | Missing policy or grant | Re-run Script 3 |
permission denied for table | Missing GRANT | Re-run the grant lines in Scripts 3 and 4 |
new row violates row-level security policy | Review failed the policy | Rating must be 1 to 5, name 1 to 50 characters, comment at most 500 characters |
relation "movie_ratings" does not exist | Script 4 not run | Run Script 4 |
relation "genres" already exists | Script 1 run twice | Ignore it; the tables are already there |
duplicate key value violates unique constraint "genres_name_key" | Script 2 run twice | Ignore it if Table Editor shows 6 genres and 13 movies; otherwise start over (below) |
more than one row returned by a subquery | Movies inserted twice, so a title now matches two rows | Start over (below) |
| GitHub Pages shows 404 | index.html not at root, or too soon | Move files to the root; wait 2 to 3 minutes |
| Site shows the old version | Browser cache | Ctrl + Shift + R |
Git and GitHub problems
| Message or problem | Likely cause | Fix |
|---|---|---|
'git' is not recognized / command not found: git | Git not installed, or VS Code opened before installing it | Install Git (Step 0.4), close all VS Code windows and open again |
Please tell me who you are | Name and email not set | Run the two git config --global lines from Step 0.5 |
fatal: not a git repository | Terminal is not in your repo folder | File → Open Folder → your cinesuper-<jsoft-id> folder, then Terminal → New Terminal |
nothing to commit, working tree clean | Files not saved, or already committed | Save with Ctrl + S (no ● on tabs) and check git status; if already committed, just git push |
Authentication failed / 403 / Permission denied | Not signed in, or pushing to someone else's repo | Sign in via the Accounts icon in VS Code (Step 0.5), push from the VS Code terminal, and check the repo is yours with git remote -v |
! [rejected] … (fetch first) | You changed something on the GitHub website | Run git pull, then git push |
LF will be replaced by CRLF | Windows line-ending notice | Harmless, ignore it |
Wrong commit message (for example Phase X: … copied literally) | The message is not the one in the PUSH box | Before pushing: git commit --amend -m "Phase 0: repo setup". Already pushed: add the correct one with git commit --allow-empty -m "Phase 0: repo setup", then git push. Use the message of the checkpoint you missed. |
| A file is missing on GitHub | It is outside the repo folder or was not saved | Move it into cinesuper-<jsoft-id> in VS Code Explorer, save, run the push routine again |
Start over cleanly
If your data is duplicated or broken, run this in a new query. It deletes all CineSuper tables and data in your project. Then run Scripts 1, 2, 3 and 4 again, once each, in order.
drop view if exists movie_ratings;
drop table if exists reviews, movies, genres cascade;Viva questions
- Difference between a primary key and a foreign key, shown in your schema.
- What happens to reviews when a movie is deleted, and why?
- Why does the
movie_ratingsview use LEFT JOIN, not INNER JOIN? - Difference between WHERE and HAVING.
- What is Row Level Security, and what happens if it is disabled?
- Why is the publishable key safe in frontend code but the service_role key is not?
- What is a view, and how does it differ from a table?
- What does
check (rating between 1 and 5)do if you insert 6? - Convert
db.from("movies").select("*").eq("language","Malayalam")into SQL. - Which normal form is your schema in? Justify.
- One example each of DDL, DML and DCL from your project.
- Trace a review from the browser form to the database row.