Total recurring cost: $0.00 / month

Setup Guide

From an empty folder to a working, multi-user school application on the public internet. Every step below is free. Nothing here asks for a credit card.

You can skip all of this and look around first. Open index.html in a browser and sign in with student@demo.school / demo1234. The app runs in offline demo mode until you add Supabase keys, with sample students, classes, assignments and grades. Demo mode stores data in that one browser only — it is for evaluation, not for a real class.
Contents
  1. Get the files onto your computer — start here
  2. Create a Supabase account and project
  3. Run the database schema
  4. Load the curriculum and answer keys
  5. Configure authentication
  6. Add your keys to the app
  7. Test registration and login
  8. Make yourself the administrator
  9. Create a class and enrol a student
  10. Assign, submit and grade
  11. Deploy to the internet for free
  12. Test from another device
  13. Running costs and free-tier limits
  14. Troubleshooting

0. Get the files onto your computer

Do this before anything else. Every later step says things like "paste supabase/schema.sql" or "edit js/config.js" — those paths only make sense once you have the project folder sitting on your own machine.

First, the mental model — there are two halves, and they stay separate
Your website filesSupabase
What it is The ~50 HTML, CSS and JavaScript files in the trading-academy folder A Postgres database and the user accounts, running in the cloud
Where it lives A folder on your computer → later copied to a free host (Cloudflare Pages) supabase.com, in your browser. You never upload files to it
What you do to it Edit one file (js/config.js), then upload the folder Paste SQL into its editor, flip a few auth settings, copy two values out
The only connection between the two halves is two lines in js/config.js. Website files never go into Supabase, and the database never goes into your folder.
A

Download and unzip the project

Download trading-academy.zip (383 KB) from the workspace of the conversation where this was built — click the file, then the download button.

Now drag that folder somewhere permanent and easy to type. The Desktop is perfectly fine:

SystemA good home for it
macOS/Users/yourname/Desktop/trading-academy
WindowsC:\Users\yourname\Desktop\trading-academy

This folder is "the project folder" that the rest of this guide refers to. Avoid leaving it inside Downloads (easy to lose) or in iCloud/OneDrive folders that sync and lock files.

B

Check you have the right folder

Open it. You should see exactly this:

ItemWhat it isDo you touch it?
index.htmlThe front page — the entry point of the whole siteNo
login.html, app.html, reset.htmlThe other pagesNo
js/All the JavaScript, including config.jsYes — step 12
css/, assets/Styling and the offline Supabase libraryNo
supabase/Three .sql files you will copy-paste into the Supabase websiteOpen and copy, never upload
docs/This guide, plus the security and testing write-upsRead
tests/, tools/, README.mdDeveloper extrasSafe to ignore or delete

Quick proof it is in the right place: double-click index.html. The app should open in your browser with an orange DEMO MODE banner, and student@demo.school / demo1234 should sign in. If that works, your folder is fine and you can start at section 1.

C

How to open a terminal inside that folder

You only need this for step 13 (testing locally). If you would rather skip it, you can go straight from section 5 to section 10, put the site online first, and do all your testing on the live URL.

SystemHow
macOS Right-click the trading-academy folder in Finder → Services → New Terminal at Folder.
Or: open Terminal, type cd  (with the space), drag the folder onto the window, press Enter.
Windows Open the folder in File Explorer, click the white address bar at the top, type cmd, press Enter.
Or: right-click the folder → Open in Terminal.

Confirm you are in the right place by listing the files — type ls on macOS or dir on Windows. You should see index.html in the output. If you do not, you are in the wrong folder and every command after this will fail.

Whenever a step says "paste the whole file" It means: open that .sql file from your project folder in a plain text editor (TextEdit on macOS, Notepad on Windows, or VS Code), select everything with Cmd+A / Ctrl+A, copy with Cmd+C / Ctrl+C, and paste it into the Supabase SQL Editor in your browser. You are copying text, not uploading a file. Nothing is dragged into Supabase at any point.

1. Create a Supabase account and project

1

Sign up

Go to supabase.com and click Start your project (top right). Sign in with GitHub or with an email address. The free plan needs no payment details.

2

Create the project

On the dashboard click New project.

Click Create new project and wait roughly two minutes while it provisions.

Never put the database password or the service-role key in this app. The browser only ever needs the project URL and the publishable key. Those two are designed to be public. The service-role key bypasses every security policy in the database — if it reaches a browser, every student can read every other student's grades, and there is no way to undo that.

2. Run the database schema

3

Open the SQL Editor

In the left sidebar of your project click SQL Editor (the icon that looks like a terminal prompt). Then click New query at the top of the panel.

4

Paste and run supabase/schema.sql

Open supabase/schema.sql — that is the supabase sub-folder inside the project folder you unzipped in section 0 — in a text editor, select everything (Ctrl+A / Cmd+A), copy it, paste it into the SQL editor and click Run (bottom right, or Ctrl+Enter).

It takes a few seconds. You should see Success. No rows returned. The script is idempotent: running it a second time is harmless.

This one file creates everything: 21 tables, 7 enum types, the authorisation helper functions, the guard triggers, Row Level Security on every table with 47 policies, 10 RPC functions, and a least-privilege grant block that strips all table access from anonymous visitors.

5

Paste and run supabase/schema-math.sql

This step is not optional. It creates the three tables the mathematics side of the app writes to. If you skip it, every maths page still loads and the practice problems still work, but nothing a student does is saved — the browser queues the attempts and keeps retrying against tables that do not exist.

Same routine as the previous step: New query → paste the whole of supabase/schema-math.sql → Run. It is safe to run twice.

The output panel ends with a NOTICE that must read:

LearnAurelle math schema: 3 / 3 tables, 8 / 8 policies, 5 / 5 functions

What it adds:

6

Confirm RLS is on

Paste this into a new query and run it:

select count(*) filter (where rowsecurity) as tables_with_rls,
       count(*)                            as tables_total
  from pg_tables where schemaname = 'public';

Both numbers must be 24 — the original 21 tables plus the three added by schema-math.sql. If tables_with_rls is lower, a schema file did not finish — scroll up in the editor output for the error and re-run it.

Practice is formative, and the database cannot verify it. Practice problems are generated and graded in the browser, so the server has no way to independently confirm that an answer marked correct really was. A determined student could post a false result. That is why practice is never a gradebook source. Anything that counts toward a grade goes through submit_quiz(), which is scored inside Postgres against an answer key the student's browser never receives. The maths attempt log exists so a teacher can see effort and spot trouble, not to assign marks.

3. Load the curriculum and answer keys

7

Run supabase/seed_curriculum.sql

Exactly the same copy-paste routine as step 4, with the other file: New query → paste the whole of supabase/seed_curriculum.sql → Run. The final row must read 36 | 36 | 0: thirty-six lessons, thirty-six answer keys, none missing.

This is what makes graded quizzes trustworthy. The correct answers live in lesson_answer_keys, which has RLS enabled and no read policy for students at all. When a student submits a graded quiz the browser sends only their chosen option numbers; public.submit_quiz() compares them inside the database and returns the score. A student who reads every line of JavaScript this app serves still cannot obtain the key.

If you change a quiz question later Edit the lesson in js/curriculum-*.js, then regenerate and re-run the seed so the database key stays in step:
node tools/gen_curriculum_sql.js > supabase/seed_curriculum.sql

4. Configure authentication

8

Turn off email confirmation while you are testing

Left sidebar → Authentication → Sign In / Providers → Email. Switch Confirm email off and click Save.

Why this matters more than it looks Supabase's built-in email sender is rate limited to roughly two messages per hour on the free plan. That limit applies to signup confirmations, magic links and password resets together. With confirmation on, the third person to sign up in an hour simply never receives their email and cannot log in. For a classroom, either leave confirmation off, or wire a free custom SMTP provider under Authentication → Emails → SMTP Settings before you invite a class.
9

Set the site URL

Authentication → URL Configuration.

10

Optional: restrict signups to your school's domain

If every student has an address at the same domain, you can block everyone else. Authentication → Sign In / Providers → Email → Allowed email domains. This is the cheapest way to stop strangers creating accounts in your school.

5. Add your keys to the app

11

Copy the two public values

The fastest way to get both at once: open your project and click the Connect button in the bar across the top of the page (next to the project name). The dialog that opens shows the Project URL and the publishable key together, with a copy button on each.

Supabase splits these two values across two different pages, so if you prefer the settings screens:

Shortcut if you cannot find it: look at your browser's address bar while the project is open. It reads https://supabase.com/dashboard/project/abcdefghijklmnop. That bold part is your project ref, and your Project URL is always https://abcdefghijklmnop.supabase.co. Do not paste the supabase.com/dashboard/… address itself into config.js — that is the dashboard, not the API.
Field on the Supabase pageGoes into js/config.js as
Project URL — https://abcdefgh.supabase.coSUPABASE_URL
Publishable key — sb_publishable_…
(on older projects: the key labelled anon public)
SUPABASE_PUBLISHABLE_KEY
service_role / secret key Nowhere. Never. Not in any file in this project.
Copy the Project URL, not the RESTful endpoint — this one bites hard The Data API page shows two similar-looking addresses. Only the short one is correct:
Label on the pageLooks likeUse it?
Project URLhttps://abcdefgh.supabase.co Yes — this one
RESTful endpointhttps://abcdefgh.supabase.co/rest/v1/ No
The library adds /rest/v1 and /auth/v1 itself. If you paste the longer one it builds …/rest/v1/auth/v1/token and …/rest/v1/rest/v1/profiles, and every login and every query fails with 404 PGRST125 — Invalid path specified in request URL. The page still loads normally, so it looks like a login bug rather than a typo in a config file.

Since this is so easy to do, js/supabase.js now strips a trailing /rest/v1, /auth/v1, /storage/v1 or /realtime/v1 automatically and prints a console warning. Fix config.js anyway so the warning goes away.
12

Edit js/config.js

window.LA_CONFIG = {
  SUPABASE_URL: "https://abcdefgh.supabase.co",
  SUPABASE_PUBLISHABLE_KEY: "sb_publishable_xxxxxxxxxxxxxxxxxxxx",

  APP_NAME:    "LearnAurelle",
  SCHOOL_NAME: "Your School Name",
  ...
};

Save the file and reload the app. The orange DEMO MODE banner disappears — that is how you know it is talking to your real project.

This file is safe to commit to a public repository. Those two values are meant to be in a browser; your data is protected by the RLS policies, not by hiding the key.

6. Test registration and login

13

Serve the folder

Opening index.html straight from disk works for a quick look, but real authentication behaves better over HTTP. Open a terminal inside the project folder (section 0, step C) and run one of these:

SystemCommand
macOSpython3 -m http.server 8080
Windowspython -m http.server 8080
Either, if you have Node instead of Pythonnpx serve -l 8080

The terminal will print something like Serving HTTP on 0.0.0.0 port 8080 and then appear to hang. That is correct — it is the server running. Leave that window open and switch to your browser:

http://localhost:8080

When you are finished, press Ctrl+C in the terminal to stop it.

"python: command not found" / "'python' is not recognized" You do not have Python installed, which is common on Windows. Either install Node.js and use the npx serve line, or simply skip this step: double-clicking index.html is enough to click through the app, and everything works properly once the site is deployed in section 10. Local serving is a convenience, not a requirement.
14

Create your own account

Click Get started → the Create account tab. Enter your name, your email and a password of at least eight characters, tick the acknowledgement and submit.

Notice what you are not offered: there is no role selector. Everyone who signs up through the public form becomes a pending student with no school attached. There is no way to register as a teacher or an administrator, and no hard-coded administrator password exists anywhere in this project.

15

Confirm the profile row exists

Supabase dashboard → Table Editor → profiles. Your row should be there with role = student and status = pending, created automatically by the handle_new_user trigger.

7. Make yourself the administrator

16

Run the bootstrap function once

SQL Editor → new query → substitute your own address:

select public.bootstrap_first_admin('you@yourschool.org');

A successful run returns: OK: you@yourschool.org is now an administrator of LearnAurelle.

You can also pass a school name and slug:

select public.bootstrap_first_admin(
  'you@yourschool.org', 'Riverside High School', 'riverside');

Sign out and back in. You now land on the School Dashboard.

Already bootstrapped under the old name? The application was called TradeDesk Academy before it became LearnAurelle. Renaming the app does not rename a school row that already exists in your database — the default only applies the first time the function runs. If you bootstrapped earlier, update the row once in the SQL Editor:
update public.schools
   set name = 'LearnAurelle', slug = 'learnaurelle'
 where slug = 'tradedesk';
Check what you actually have first with select id, name, slug from public.schools; — if the row is already named after your real school, leave it alone. The school name is what students see in the sidebar and in the footer of emailed notifications.
Why this is safe bootstrap_first_admin has EXECUTE revoked from public, anon and authenticated. It can only be called from the SQL editor or another trusted connection — never from the browser. It also refuses to run if the school already has an active administrator, so it cannot be used to seize an existing school. Every call is written to audit_logs.
17

Optional: load the sample classes

Run supabase/seed.sql in the SQL editor. It creates three classes with working join codes, nine assignments covering every activity type, and some announcements. It never creates user accounts — those have to come from real signups. It is safe to run more than once.

8. Create a class and enrol a student

18

Invite a teacher

In the app: Users → Invite teacher/admin. Enter their email and choose Teacher. If they have already signed up they are promoted immediately; if not, the invitation applies the moment they register with that address.

Every role change is recorded in Audit Logs, including yours.

19

Create a class

Classes → Create class. Give it a name and pick a lead teacher. A teacher who is not listed on a class cannot see it at all — that is checked by public.is_class_teacher() inside every relevant policy, not by hiding buttons.

20

Enrol students — two ways

Codes are validated in the database by public.join_class_with_code(), which also checks that the code has not been revoked. Press Rotate code and the old one stops working for everybody instantly; students already enrolled are unaffected.

21

Approve pending signups

A brand-new account has no school until you approve it. Users → Approve next to their name. Until then they can read the Learning Academy but see no class work.

Approving does two things in a single database write: it sets the person's school_id to your school and flips status from pending to active. Both halves matter — see the warning below.

Where to find people waiting: the Overview page shows an amber "N accounts awaiting approval" banner, and Users → Any status → Pending lists them. A person with no school is also tagged not in a school yet under their name.

If you deployed before 15 Oct 2026, update your files first An early build of js/supabase.js sent only status: "active" and dropped school_id. That combination is invisible to every branch of can_view_profile: the approval-queue branch requires status='pending' and the same-school branch requires a matching school_id. An account approved by that build vanishes from the Users page and cannot be repaired from the interface. The current files always send both fields. To find and rescue anyone already stranded, run this in the Supabase SQL Editor (which runs as the table owner, so the guard trigger steps aside):
-- who is stranded?
select id, email, full_name, role, status
from public.profiles
where school_id is null and status <> 'pending';

-- put them back in the approval queue, then click Approve in the app
update public.profiles set status = 'pending'
where school_id is null and status = 'active';

9. Assign, submit and grade

22

As the teacher: create an assignment

Create Assignment → Open the assignment form. Set a title, class, points and due date. Add rubric criteria if you want criterion-level marking. Pick an activity type — Lesson quiz is auto-graded by the database; the rest are graded by you.

Save it as a draft first if you like: students never see drafts. Click Publish now when it is ready.

To target only some students, open the assignment and use Assign to…. Students who are not targeted cannot load it even if they guess the address.

23

As the student: submit

Assignments → open it → type an answer → Save draft (drafts live in the database, so they follow the student to any device and auto-save every 25 seconds) → Submit.

24

As the teacher: grade and release

Submissions → Grade. The student's work is on the left, the marking form on the right. Enter rubric scores (the total fills in automatically), write private feedback, and choose:

A held-back grade is marked with an asterisk in the gradebook. Unreleased grades are invisible to students at the database level: the students' SELECT policy on grades includes and is_released, so the row is never sent to their browser.

25

Check the student side

Sign in as the student and open Grades & Feedback. The released grade, the percentage, the letter and the written feedback are all there. The Export CSV button downloads it.

10. Deploy to the internet for free

This app is plain HTML, CSS and JavaScript with no build step, so any static host will serve it. Cloudflare Pages is used below because its free tier has unlimited bandwidth.

Already own a domain? Deploy with Option A or B first to get a working .pages.dev address, then jump to Putting it on your own domain below. It is free — free DNS, free HTTPS certificate, no plan upgrade.

Option A — drag and drop (no GitHub, no command line, two minutes)

Upload the learnaurelle-site folder, not the whole project folder.

Cloudflare's dashboard uploader refuses any folder that contains a Wrangler config file, and the full project folder has one (wrangler.jsonc). If the upload area rejects your folder, or complains about Wrangler, this is why. The learnaurelle-site folder is the same website with the developer files stripped out, so the uploader has nothing to object to.

You do not need the Wrangler command-line tool to publish this site, and you never need to install Node, npm or npx.

26

Create a free account at dash.cloudflare.com. In the sidebar choose Workers & Pages → Create → the Pages tab → Upload assets. Give the project a name — learnaurelle is fine — then drag the learnaurelle-site folder onto the upload area and click Deploy site.

This is the moment your files leave your computer — the folder is copied to Cloudflare's servers, and that copy is what the world sees. Supabase is untouched by this; it has been holding your database the whole time.

You get a URL like https://learnaurelle.pages.dev.

Three things that trip people up here
  • Drag the folder, not the individual files inside it. Cloudflare needs index.html to end up at the top level of the site; dragging the folder does that correctly. If you were given learnaurelle-site.zip, unzip it first — the folder it produces is the thing to drag.
  • Make sure you already edited js/config.js (step 12) before uploading. You are uploading a snapshot; if you edit the file on your computer afterwards, re-upload to publish the change.
  • Do not drag the project folder that contains wrangler.jsonc, supabase/, tests/ and tools/. That is the working copy, not the website.

To publish a change later, return to the same project and choose Create a new deployment, then drag the folder again. The new upload replaces the old one.

What is actually in learnaurelle-site

The four pages (index, login, app, reset), css/, js/, assets/, and a docs/ copy of this guide plus the security and testing notes. No SQL, no tests, no build tooling, no Wrangler config. Everything in it is meant to be public — js/config.js carries only your Supabase project URL and publishable key, both of which are designed to be read by browsers. The database enforces permissions on its own; see the security model.

Option B — connect GitHub (auto-deploys on every push)

27

Push the folder to a GitHub repository, then in Cloudflare choose Workers & Pages → Create → Pages → Connect to Git, pick the repository and leave the build settings empty:

SettingValue
Framework presetNone
Build commandleave blank
Build output directory/

Click Save and Deploy. Every future git push redeploys automatically.

Putting it on your own domain

Custom domains are free on every host below — free DNS, free automatic HTTPS certificate, no plan upgrade. The domain registration you already pay for is the only cost, and nothing here adds to it. Do not buy "web hosting" from your registrar. GoDaddy, Namecheap, Hostinger and the rest will try to sell you cPanel hosting at roughly $3–15/month; this app is static files and needs none of it.

You own learnaurelle.com, learnaurelle.org and tradingdesk.me There is a walkthrough written for exactly that set-up — which domain should be canonical, the leftover IONOS records to delete on each, how to attach the .com to the Worker, and how to redirect the other two without breaking email: DOMAINS.html. Start there.

The older DOMAIN-tradingdesk-me.html still documents the original IONOS → Cloudflare migration and its troubleshooting section, which is worth keeping for reference. Both are more useful than the generic steps below.

There is exactly one decision to make, and it changes the steps:

What you want in the address barWhat it costs you in effort
academy.yourdomain.com
(a subdomain)
One CNAME record at whoever runs your DNS today. Nothing else changes — your website, email and existing records are untouched. Five minutes.
yourdomain.com
(the bare apex domain)
Cloudflare requires the domain to be a Cloudflare zone, which means moving your nameservers to Cloudflare. Still free, but Cloudflare then runs all of your DNS. Alternatively use GitHub Pages, which does apex with plain A records and no nameserver change.
If you are not sure, pick the subdomain academy.yourdomain.com is the lowest-risk option by a wide margin: it cannot break your existing website or email, and you can undo it by deleting one DNS record.

Path 1 — subdomain on Cloudflare Pages (recommended)

D

Deploy first using Option A or B above, so you have a working something.pages.dev address. Then:

  1. In Cloudflare: Workers & Pages → your project → Custom domains → Set up a domain.
  2. Type academy.yourdomain.com → Continue.
  3. Cloudflare shows you the record to create. Go to your current DNS provider (wherever you bought the domain) and add it:
    TypeName / HostValue / Points toTTL
    CNAMEacademy your-project.pages.devAuto / 3600
    Most registrars want just academy in the Name field, not the full academy.yourdomain.com — they append the domain for you.
  4. Back in Cloudflare, wait for the status to turn Active. The HTTPS certificate is issued automatically and free. DNS can take anywhere from two minutes to a few hours.

Path 2 — apex domain on Cloudflare Pages

E

Add the domain to Cloudflare as a zone: in the dashboard click Add a domain, enter yourdomain.com, choose the Free plan, and let Cloudflare scan your existing DNS records. Then change the nameservers at your registrar to the two Cloudflare gives you. Once the zone is active, go to your Pages project → Custom domains → Set up a domain → enter yourdomain.com, and Cloudflare writes the record itself.

Check your email records before you switch nameservers Moving nameservers moves all DNS for that domain. If you receive email at that domain (Google Workspace, Microsoft 365, or anything else), confirm the MX, TXT/SPF, DKIM and DMARC records were all copied into Cloudflare's scan before you change the nameservers at your registrar. A missed MX record means mail stops arriving, and that is the one mistake here with real consequences.

Path 3 — apex domain without touching your nameservers (GitHub Pages)

F

If you want the bare domain but will not move DNS, GitHub Pages handles apex with ordinary A records. Push the project to a public GitHub repository, then repository Settings → Pages → set the source branch, and under Custom domain enter yourdomain.com and save. At your DNS provider add these four A records, all with the name @:

185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153

Then tick Enforce HTTPS in the same GitHub settings page — it can take up to 24 hours to become available while the certificate is issued. GitHub Pages is free with a 1 GB site limit and a soft 100 GB/month bandwidth limit, both far beyond what this app uses.

Which free host to use

HostCostBandwidthCustom domain + HTTPSApex without moving nameservers?
Cloudflare Pages$0UnlimitedFree, up to 100 domains per projectNo — apex needs a Cloudflare zone
GitHub Pages$0100 GB/mo soft limitFreeYes, via A records
Netlify$0Credit-based since Sept 2025; free tier ≈ 15 GB/moFreeYes

Cloudflare Pages is the recommendation because it is the only one of the three with no traffic cap at all, which removes any chance of a surprise bill or a suspended site. Netlify's free plan moved to a credit system and is now the tightest of the three.

Optional, and only if you want to save money on the domain itself Cloudflare Registrar sells domains at wholesale cost with no markup and no first-year-cheap, fifth-year-expensive games. If your domain renews somewhere expensive, transferring it to Cloudflare usually cuts the renewal price. This is entirely optional and unrelated to hosting — your current registrar works fine.
28

Point Supabase at the final address — do not skip this

Supabase has to be told which address your app lives at, or email confirmation links and password resets will quietly fail. Go to Authentication → URL Configuration.

The two fields do different jobs — this is the most misread screen in the whole setup
FieldHow many valuesWhat it is
Site URLExactly one Your canonical address. Supabase inserts it into confirmation and password-reset emails.
Redirect URLsA list An allow-list of every address permitted to receive a login redirect. Anything not listed is refused.
They are not a "main domain" box and a "subdomain" box. One canonical address goes in the first; every address the app can be reached at goes in the second.
FieldWhat to enter
Site URL Whatever address people will actually use. If your custom domain is live, that (https://yourdomain.com). If it is not connected yet, use the host's address (https://your-site.pages.dev or https://your-worker.workers.dev) and change it later.
Redirect URLs Add every one of these that applies, one per line:
https://yourdomain.com/**
https://www.yourdomain.com/**
https://your-site.pages.dev/** — keep it as a fallback
http://localhost:8080/** — keep it if you test locally

The trailing /** is a wildcard that covers every page on that address, so you do not have to list /reset.html and the rest one by one. Listing an address that is not live yet is harmless. Password resets fail silently when this is wrong, so test one reset after changing it.

Any static host works There is no build step and no server-side code in this project, so Vercel, Render, Firebase Hosting or a plain Apache box would all serve it equally well. Pick whichever you already have an account with — just redo step 28 with whatever address you end up on.

11. Test from another device

29

On a phone, open the deployed URL and sign in as the student account. You should see the same classes, the same drafts and the same grades you saw on the desktop — because the data lives in Postgres, not in the browser. That is the actual test of whether the app is really multi-user.

Things worth checking on a phone:

If you only ever test in one browser you have tested nothing. Demo mode is per-browser by design. The moment real Supabase keys are in place, open two different browsers (or one normal and one private window) and sign in as two different people. That is the only way to see the permission boundaries actually working.

12. Running costs and free-tier limits

ServicePlanCostWhat you get
SupabaseFree$0 500 MB database, 1 GB file storage, 5 GB egress, 50,000 monthly active users, 500,000 Edge Function calls, 2 active projects per organisation
Cloudflare PagesFree$0 Unlimited bandwidth, 500 builds per month, global CDN, automatic HTTPS
GitHubFree$0Optional. Unlimited public and private repositories
Market data—$0 None purchased. All prices are generated locally and labelled synthetic
Total$0.00 / month
Three free-tier behaviours that will surprise you

The paid Supabase Pro plan is $25 per organisation per month, which is well over the $10 budget this project was built to. Nothing in this application requires it.

13. Troubleshooting

SymptomCause and fix
Orange DEMO MODE banner will not go away js/config.js still has empty strings, or the file was not saved. Hard-reload with Ctrl+Shift+R.
"No profile found" after signing in The handle_new_user trigger is missing — schema.sql did not finish. Re-run it; it is safe to run twice.
Signing up returns "error sending confirmation email" You have hit the ~2 emails/hour limit. Turn off email confirmation (step 8) or add custom SMTP.
Teacher sees "You are not assigned to any class" Correct behaviour. An administrator must add them on the class page under Teachers. The database returns nothing until then.
Student sees no assignments They are not enrolled, their account is still pending, or the assignment is a draft. Check all three in that order.
Password reset email link does nothing /reset.html is not in the Redirect URLs list (step 9).
"permission denied for table …" RLS is working. Either the signed-in user genuinely may not do that, or they need to be enrolled/authorised first. Check Audit Logs and the user's role.
Everything is slow after a holiday The project paused. Open the Supabase dashboard and click Restore project.