Appearance
Custom domains & white-labeling
Put a school on its own web address, with its own login design, its own logo and its own name. Nothing on screen says the software came from you.
This is the feature schools pay more for. It is worth doing properly.
What the school gets
Instead of sending parents to your platform address, the school sends them to portal.theirschool.edu — their domain, their logo, their colours, their login screen. As far as parents and staff are concerned, the school built it.
For you it is the same single installation. Nothing is duplicated.
Two ways to do it
Decide this once, before your first school asks.
| A — A subdomain of your platform | B — The school's own domain | |
|---|---|---|
| Looks like | stmarys.yourplatform.com | portal.stmarys.edu |
| Work per school | None — type the name and save | DNS, web server and SSL each time |
| Who does the work | You, once, for all schools | You and the school, per school |
| Ready in | Seconds | Hours to two days, mostly waiting for DNS |
| Sells as | Included | Premium add-on |
Set up A once, offer B as the upgrade
The wildcard setup below takes twenty minutes and then every school you ever add gets its own address instantly, with no DNS and no certificate work. Charge for B, where the school insists on its own domain.
Route A — one wildcard, every school covered
Do this once on your platform domain.
1. One DNS record
| Type | Host / Name | Value |
|---|---|---|
A | * | your server's public IP |
Find the IP by running curl ifconfig.me on the server.
2. Tell Nginx to accept any subdomain
nginx
server_name yourplatform.com *.yourplatform.com;3. One wildcard certificate
bash
sudo certbot certonly --manual --preferred-challenges dns \
-d "yourplatform.com" -d "*.yourplatform.com"Certbot asks you to add an _acme-challenge TXT record. If your DNS is on Cloudflare, use the certbot-dns-cloudflare plugin so renewals happen automatically — a manual wildcard certificate must otherwise be renewed by hand every 90 days.
Done. From now on, set any school's custom domain to theirname.yourplatform.com and it works immediately. No DNS, no certificate, no server change.
Route B — the school's own domain
Four steps, in order. The panel walks you through the same four with your real values filled in: open a school and choose Open the Domain Setup Guide.
Step 1: DNS
The school adds one record at their domain registrar — GoDaddy, Namecheap, Cloudflare, wherever the domain lives.
| Option | Type | Host / Name | Value |
|---|---|---|---|
| A — root or subdomain (recommended) | A | @ or the subdomain | Your server's public IP |
| B — subdomain only | CNAME | portal | Your platform hostname |
DNS usually updates in 5–30 minutes, but is allowed to take up to 48 hours. Check with:
bash
nslookup portal.theirschool.eduWhen that returns your server's IP, continue. Not before — the next steps fail confusingly if DNS has not caught up.
Step 2: Web server
On shared hosting (hPanel / cPanel) there is no config to edit. Attach the domain to the account instead:
- Open Domains → Aliases (older cPanel calls these "Parked Domains"), or Domains → Create a New Domain
- Add the domain and point its Document Root at the same folder as this app
- The DNS record from step 1 must already be live, or the panel refuses to attach it
- Run SSL/TLS Status → AutoSSL to issue the certificate
Some hosts require domain-ownership verification first — the school completes that at their registrar.
On Nginx, the quickest route is to add the domain to the existing server block:
nginx
server_name yourplatform.com portal.theirschool.edu;If your Nginx block already uses server_name _; (catch-all), the domain works as soon as DNS points at the server — skip straight to SSL.
For a dedicated vhost, block order matters:
nginx
server {
listen 80;
server_name portal.theirschool.edu;
root /var/www/school-erp/public; # same root as the main app
index index.php;
# 1. CRITICAL: private media (student documents, certificates, ID cards)
# is served through Laravel with signed URLs — never as a static file.
location ^~ /media/private/ {
try_files $uri $uri/ /index.php?$query_string;
}
# 2. Main Laravel routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 3. Static caching — must come AFTER the exclusion above
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
expires 30d;
access_log off;
}
}On Apache, the simplest option is one line added to the existing VirtualHost:
apacheconf
ServerAlias portal.theirschool.eduThen reload:
bash
sudo nginx -t && sudo systemctl reload nginx # Nginx
sudo systemctl reload apache2 # ApachePanel-managed servers: read this or certificates and documents will 404
aaPanel, CyberPanel and Forge generate a vhost that already contains an image and PDF caching rule. If the location ^~ /media/private/ block is missing — or sits below that caching rule — student documents, generated ID cards and certificates return 404, while logos and ordinary images work perfectly.
It must be block number one, above everything else.
Step 3: SSL
Once DNS resolves to your server:
bash
sudo certbot --nginx -d portal.theirschool.edu # Nginx
sudo certbot --apache -d portal.theirschool.edu # ApacheCertbot configures the certificate and the HTTP→HTTPS redirect. Confirm renewal works:
bash
sudo certbot renew --dry-runStep 4: Check, then set it Active
Open https://portal.theirschool.edu. You should see that school's branded login.
Then open a page containing a student photo or document — this proves private media is being served correctly. If those 404 while the rest of the page loads, go back to step 2.
Only then set Domain Status to Active (Live) and save.
The white-label part
Setting the domain is half of it. The other half is the login screen.
Each school picks a Login Design Style:
| Style | Look |
|---|---|
| System Default (Classic) | Warm orange marketing page with quick role tiles |
| Campus Pro (Institutional) | Clean university-grade split screen, adopts the school's brand colour, mobile-first |
| Modern Split (Premium) | Bold brand panel left, clean white form right |
| Centered Glassmorphism (Modern) | Dark slate backdrop, frosted-glass centred card |
| Aurora Night (Immersive) | Animated aurora gradients over a deep night sky |
| Minimal Luxe (Elegant) | Ivory whitespace, serif headline, gold accent |
| Custom Design (Your Own) | Build the layout and colours yourself |
Changes apply immediately — the school does not need to log out. Each style has a Live Preview before you commit.
On the school's own domain the login uses their logo and name. On your platform address it keeps your branding.
Changing a school's slug breaks links already in circulation
The public admission link contains the slug — /apply/school-name. Parents may already have that link, and schools put it on posters and WhatsApp.
Change the slug and every one of those links dies, silently. Decide the slug when the school is created.
If it is not working
| What you see | Cause | Fix |
|---|---|---|
| Domain does not load at all | DNS has not propagated, or points elsewhere | nslookup the domain — it must return your server's IP |
| Your platform's login appears, not the school's | Domain Status is not Active, or the domain is typed differently in the school record | Check for typos and set the status to Active |
| Certificate warning in the browser | No SSL issued for this domain | Run certbot for that exact domain, or AutoSSL on shared hosting |
| Login page fine, but student photos and certificates 404 | Private media rule missing or below the caching rule | Step 2 |
| cPanel refuses to add the domain | DNS not live yet, or ownership not verified | Wait for DNS, then retry |
| Wildcard subdomain stopped working after 90 days | A manually issued wildcard certificate expired | Automate renewal with the DNS plugin |