Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
YouTube Security VideosOpenAI: “It Blew Me Away” | Box’s First Look at GPT-6 Astra(21.09.2026 um 18:13 Uhr)
YouTube Security VideosGoogle Ads: Win the holiday season in 90 seconds | Rethink Retail 2026(21.09.2026 um 18:13 Uhr)
YouTube Security VideosPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Videos & KonferenzenMariaDB Foundation: The Fork Series #1 - The Future of Databases(21.09.2026 um 19:18 Uhr)
Videos & KonferenzenPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Unix & Linux ServerSecurity: Denial of Service in Memcached (Ubuntu)(21.09.2026 um 19:21 Uhr)
YouTube Security VideosOpenAI: “It Blew Me Away” | Box’s First Look at GPT-6 Astra(21.09.2026 um 18:13 Uhr)
YouTube Security VideosGoogle Ads: Win the holiday season in 90 seconds | Rethink Retail 2026(21.09.2026 um 18:13 Uhr)
YouTube Security VideosPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Videos & KonferenzenMariaDB Foundation: The Fork Series #1 - The Future of Databases(21.09.2026 um 19:18 Uhr)
Videos & KonferenzenPC-WELT: 5 crazy Sitzpositionen, die jeder Gamer kennt 😀(21.09.2026 um 18:52 Uhr)
Unix & Linux ServerSecurity: Denial of Service in Memcached (Ubuntu)(21.09.2026 um 19:21 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building and Deploying a Role-Based Django PWA: From Starter Template to Production

I recently built a full volunteer-opportunity board on top of a Django starter template — complete with role-based accounts, image uploads, a Progressive Web App layer, and a real production deployment. This post walks through what I b…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

I recently built a full volunteer-opportunity board on top of a Django starter template — complete with role-based accounts, image uploads, a Progressive Web App layer, and a real production deployment. This post walks through what I built, the decisions behind it, and — more usefully — the deployment problems I actually hit and how I solved them, since that's the part tutorials usually skip.



Live app: https://volunteer-board-nccd.onrender.com

Source code: https://github.com/13getid/django-starter



Demo accounts if you want to poke around without signing up:







The starting point



Rather than building a Django project from a blank startproject, I started from a SaaS Pegasus-derived starter template that already had a lot of the tedious groundwork done: django-allauth for authentication, a custom user model with an avatar field, Tailwind v4 + DaisyUI wired up through Vite, DRF for an API layer, and a production-ready Docker Compose stack.



This turned out to be the right call. Registration, login, password change, and even a "change picture" avatar upload were already working the moment I ran the dev server. That meant I could spend my time on the actual product instead of re-implementing auth from scratch.





What I built on top of it



The concept: a volunteer opportunity board. Organizations post opportunities; volunteers browse and sign up.





Two roles, enforced for real



I added a role field to the custom user model:




class CustomUser(AbstractUser):
class Role(models.TextChoices):
ORGANIZATION = "organization", "Organization"
VOLUNTEER = "volunteer", "Volunteer"

role = models.CharField(max_length=20, choices=Role.choices, default=Role.VOLUNTEER)

@property
def is_organization(self) -> bool:
return self.role == self.Role.ORGANIZATION






The interesting part wasn't the field — it was hooking it into django-allauth's signup flow without fighting the library. Allauth's SignupForm has a custom_signup(request, user) hook that runs automatically right after the user is created, which is exactly the right place to persist a field that isn't part of allauth's own form:




class TermsSignupForm(TurnstileSignupForm):
role = forms.ChoiceField(choices=CustomUser.Role.choices, widget=forms.RadioSelect)

def custom_signup(self, request, user):
user.role = self.cleaned_data["role"]
user.save(update_fields=["role"])






Role isn't just cosmetic — every opportunity-related view checks it server-side:




@login_required
def opportunity_create(request):
if not request.user.is_organization:
raise PermissionDenied("Only organizations can post opportunities.")






A volunteer account hitting /opportunities/new/ directly gets a real 403, not just a hidden button.






The opportunities app



A standard Django app with two models — Opportunity (title, description, location, date, cover image, spot count) and SignUp (a through-model linking a volunteer to an opportunity, with a unique_together constraint so you can't double-sign-up). List view supports search and location filtering; detail view has a sign-up/cancel toggle; and each role gets its own dashboard.






Progressive Web App



Three pieces, deliberately simple:





  1. Manifest — the starter already shipped icon assets and a site.webmanifest, it just needed name, start_url, and scope filled in.


  2. Service worker, served via Django's TemplateView at the site root (/sw.js) rather than through Vite's /static/ path — service workers need root scope to control the whole site.


  3. Offline fallback page — the service worker's fetch handler falls back to a custom "You're offline" page on navigation failures.




self.addEventListener("fetch", (event) => {
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request).catch(() => caches.match(OFFLINE_URL))
);
}
});









Deployment: where the real learning happened



I deployed to Render's free tier — no credit card required, real Postgres, real persistent web service. The Docker build itself went smoothly on the first attempt, which was almost suspicious in retrospect, because everything after that surfaced one infrastructure lesson after another.






Lesson 1: free-tier disk is ephemeral



The starter's production Docker Compose uses a named volume for media uploads. Render's free web service disk doesn't persist that way — anything written locally gets wiped on redeploy. Avatars and opportunity photos would silently vanish. Fix: route media storage through Cloudinary instead of local disk, gated behind an environment variable so local development still uses the filesystem:




# prod.py
if "CLOUDINARY_URL" in env:
STORAGES["default"]["BACKEND"] = "cloudinary_storage.storage.MediaCloudinaryStorage"









Lesson 2: Docker Command quoting is fragile



My first deploy attempt used a single chained shell command (migrate && collectstatic && exec gunicorn ...) directly in Render's "Docker Command" field. It failed with command not found — the platform's command-parsing didn't like the inline && chaining. The fix was more reliable than debugging the quoting: move the whole thing into an actual shell script file committed to the repo, and point the Docker Command at that instead.




#!/usr/bin/env bash
set -e
python manage.py migrate --noinput
python manage.py collectstatic --noinput
exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3









Lesson 3: outbound SMTP ports are blocked



This was the interesting one. I configured Brevo (a transactional email service) over SMTP, and the connection just... hung. No error, no timeout message — the request would eventually die when Gunicorn's worker timeout killed it. The traceback pointed straight at socket.connect() never returning.



It turns out many free-tier PaaS providers block outbound SMTP ports (25, and often 587 too) as a spam-prevention measure. This isn't documented prominently anywhere obvious — I found it by elimination, after confirming the credentials, the sender verification, and the Django settings were all correct, and the connection was still just hanging.



The fix: switch from SMTP to Brevo's HTTP API instead, using django-anymail's Brevo backend, which the starter already had installed:




EMAIL_BACKEND = "anymail.backends.brevo.EmailBackend"
ANYMAIL = {"BREVO_API_KEY": env("BREVO_API_KEY", default="")}






HTTP traffic over port 443 isn't blocked the way SMTP is, and the switch fixed it immediately — email sending went from "hangs forever" to "sub-second response."






Lesson 4: Gmail is suspicious of Gmail



Even after switching to the API, one specific case still deferred: sending from a @gmail.com address (verified in Brevo) to another @gmail.com address. Brevo's delivery logs showed the exact reason:




421-4.7.28 Gmail has detected an unusual rate of mail originating from your SPF...




This is Gmail's own anti-spoofing protection — it's inherently wary of mail claiming to be "from Gmail" that didn't actually come through Google's infrastructure, since that's a common phishing pattern. It's a soft, temporary deferral rather than a hard bounce, and email to non-Gmail addresses delivers cleanly. The durable fix is authenticating a real custom domain as the sender rather than relying on a free Gmail address — outside the scope of what a demo project needs, but worth documenting honestly rather than pretending it doesn't exist.






What I'd do differently



If I were starting this project without a deadline, I'd set up the custom email domain from the start rather than retrofitting it, and I'd reach for Render's paid tier's Shell access earlier — debugging blindly through commit-push-check-logs cycles for the SMTP issue cost more time than a five-minute interactive shell session would have.






Where this is now, and what's next



I want to be upfront about something: the UI you'll see on the live demo right now is a starting point, not a finished design. I've reworked the landing page into a dark themed layout, redesigned the auth pages, and added stat-card dashboards. Mobile responsiveness was actually broken partway through — the nav bar was overflowing on small screens — and I've since fixed and confirmed it works properly on an actual phone. But there's real design work still ahead: pushing the dark theme more consistently across every page, tightening spacing and color balance, and generally giving the whole thing a more considered visual pass than what a first build gets.



I'm treating this as a living project rather than a one-and-done submission. Design and UX are iterative, and I'd genuinely rather ship something functional now and keep refining it in the open than sit on it chasing pixel-perfection before anyone sees it.



If you look at this and have thoughts — a layout you'd approach differently, a color palette that would work better, an accessibility issue I've missed, a feature that would make the opportunity board more useful — I'd love to hear it. Suggestions are genuinely welcome, and so is collaboration — open an issue, leave a comment, or send a PR. This is exactly the kind of small project that benefits from more eyes on it.






Closing thoughts



Starting from a solid template made the auth/user/styling groundwork nearly free, which meant almost all my actual engineering time went into the parts that mattered: the role-permission model, the opportunity/sign-up feature, and — unexpectedly — a genuinely useful lesson in how free-tier cloud infrastructure quietly blocks things you'd never think to check until they silently fail.



If you're working on something similar, the one thing I'd flag loudest: when something fails with no error message at all, especially anything network-related, suspect the platform's outbound rules before you suspect your code.






Repo: https://github.com/13getid/django-starter — feel free to look through the commit history, it roughly tells this whole story in order. Issues and PRs welcome.

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building and Deploying a Role-Based Django PWA: From Starter Template to Production

Thematisch verwandte Begriffe: Building, Deploying, RoleBased, Django · 6 Treffer

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-82412 | ntopng is a web-based network traffic monitoring application. Prior to 6…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick