A modern, performant community web application built with Astro, TypeScript, and React — live at mahalle.digital, serving the Schillerkiez neighborhood in Berlin-Neukölln.
📜 Read the Mahalle Manifesto — the project's moral charter. It describes who Mahalle is for, what it must remain, and what it refuses to become. Anyone deploying, forking, or contributing to this project is expected to honor it.
- Framework: Astro 5.x with Hybrid SSR/SSG
- UI: React 18.2 + Svelte 5 for interactive islands
- Styling: Tailwind CSS 3.4 with kiosk design system (paper-warm + ink borders — migration from legacy dark-glass completed July 2026)
- Animation: Motion 12.x (
motion/react) + Web Animations API foris:inlinescripts - State Management: TanStack Query for server state, local
useStatefor UI (no Zustand/Redux) - Data Fetching: TanStack Query 5.17 with localStorage persistence (24h)
- Database: MongoDB 6.3 (direct driver, no Mongoose)
- Authentication: auth-astro with NextAuth (Credentials provider, JWT strategy)
- Deployment: Vercel (serverless, functions pinned to
fra1— co-located with the Frankfurt Atlas cluster) - Validation: Zod schemas
- Language: TypeScript
src/
├── components/ # React (.tsx) and Svelte (.svelte) components
├── layouts/ # Astro layouts (BaseLayout.astro)
├── pages/ # File-based routing
│ ├── api/ # API routes (serverless functions)
│ │ ├── auth/ # Registration endpoint
│ │ ├── topics/ # Forum CRUD
│ │ ├── events/ # Calendar events CRUD
│ │ ├── announcements/
│ │ ├── recommendations/
│ │ ├── comments/
│ │ ├── likes/
│ │ ├── views/
│ │ ├── news/ # Newsboard CRUD, daily fetch, save/unsave
│ │ ├── listings/ # Marketplace listings CRUD
│ │ ├── reports/ # User report submission
│ │ ├── admin/ # Admin moderation APIs
│ │ ├── kiez-stats.ts # Schillerkiez demographics + social API
│ │ └── kiez-air.ts # Live BLUME air quality proxy
│ └── *.astro # Page components
├── hooks/
│ └── api/ # TanStack Query hooks
├── lib/
│ ├── mongodb.ts # Database connection
│ ├── auth.ts # Auth utilities
│ ├── moderation.ts # AI moderation + profanity filters (TR/EN/DE) + leetspeak
│ └── queryUtils.ts # Query helpers
├── schemas/ # Zod validation schemas
├── styles/ # Global CSS (dark-glass utilities, carved titles)
├── types/ # TypeScript types
└── utils/ # Helper functions
The app has completed its migration (July 2026) from a dark-glass aesthetic (deep indigo #0e1033 + purple radial gradient + glass surfaces) to a kiosk aesthetic (paper-warm surfaces, ink borders, print shadows, carved-italic title accents, DM Mono kickers, Instrument italic copy).
| Surface | State |
|---|---|
Landing „Das Schaufenster" (/, plus /impressum, /datenschutz) |
✅ Kiosk (Svelte island, own LandingLayout) — public front door, members SSR-redirect to /forum |
Forum (/forum, /topics/[id], /announcements/[id], /recommendations/[id]) |
✅ Kiosk (Svelte) |
Calendar (/calendar, /events/edit/[id]) |
✅ Kiosk (Svelte) |
Newsboard (/newsboard, /newsboard/[id], /newsboard/submit) |
✅ Kiosk (Svelte) |
Marketplace (/marketplace, /marketplace/[id], /marketplace/create, /marketplace/edit/[id]) |
✅ Kiosk (Svelte) |
Auth (/login, /register, /forgot-password, /reset-password, /verify-email) |
✅ Kiosk (Svelte, own AuthLayout) |
Admin moderation (/admin/moderation) |
✅ Kiosk (Svelte, own AdminLayout) |
Admin announcements (/admin/announcements) |
✅ Kiosk (Svelte, own AdminLayout) |
Profile (/profile, /nachbarn/[handle], /steckbrief) |
✅ Kiosk (Svelte) — complete (Plan A: own profile; Plan B: public neighbor profiles, e-mail/password change, account deletion) |
Kiez Data (/schillerkiez, /schillerkiez/druck) |
✅ Kiosk (Svelte) — legacy carousel dashboard removed |
Blog (/blog, /blog/[slug], /blog/tag/[tag]) |
✅ Kiosk (Svelte, own StandardLayout/HeroLayout/GalleryLayout) — "Die Beilage", SSR on every route, legacy dark-glass blog removed |
Each migrated page has its own accent color used for kickers (mono-uppercase eyebrows) and carved-italic title accents:
| Page | Accent |
|---|---|
| Forum | Wine #b23a5b |
| Calendar | Teal #3f8f9f |
| Newsboard | Ink #1b1a17 |
| Marketplace | Wine #b23a5b (kickers) + Ochre #eccc6e (italic headline accents only) |
| Auth | Ochre #e8a53a |
| Admin | Plum #6f2f59 |
| Profile | Ochre #e8a53a (shared with Auth) |
| Kiez Data | Moss #6b8a4a |
| Blog | Rust #a3552e |
Semantic accents stay constant across all kiosk surfaces (never swapped per page): live-now indicator (ochre dot), today indicator, weekend-day labels, required-field asterisks, compose step numbers (01, 02, …), CTA wine-shadows, modal wine-shadows, the mobile wine FAB.
KioskLayout/KioskFooter— page chrome with built-in clearance for sticky bottom barsKioskReportModal— paper-warm community-report modal (forum + calendar, reusable for upcoming kiosk surfaces)OwnStatusBanner— author-facing moderation banner (pending / reported / rejected, with optional rejection-reason blockquote)- Sonner toasts re-skinned via
unstyled: true+.kiosk-toast*classes (paper-warm bg, ink-2 border, Bricolage font, Instrument italic descriptions, print-shadow per type) GlassFilters.astro— shared SVGfeTurbulencefilters for liquid-glass refraction (used by legacy.glass-luxe*utilities on unmigrated pages)
Utilities in global.css: .dark-glass-bg, .dark-glass-gradient (fixed background divs), .carved-title (beveled text with --carved-accent CSS var), .glass-luxe, .glass-luxe-edge, .glass-smooth, .glass-smooth-edge, .glass-inner-glow. Blog is fully migrated as of July 2026 and no longer uses these.
- Teal:
#4b9aaa - Wine/Burgundy:
#814256 - Gold:
#eccc6e - Beige:
#aca89f
-
Install pnpm (if not already installed):
npm install -g pnpm # or corepack enable
-
Install dependencies:
pnpm install
-
Set up environment variables: Copy
.env.exampleto.envand fill in your values:AUTH_SECRET=your-nextauth-secret AUTH_TRUST_HOST=true NEXTAUTH_URL=https://mahalle.digital # canonical origin — REQUIRED in prod (password-reset links are built from it; forgot-password fails closed without it) MONGODB_URI=your-mongodb-uri # db name rides in the URI path: /mahalle (prod) vs /mahalle-dev (local dev + previews) CLOUDINARY_CLOUD_NAME=your-cloudinary-name CLOUDINARY_API_KEY=your-api-key CLOUDINARY_API_SECRET=your-api-secret OPENAI_API_KEY=your-openai-key RESEND_API_KEY=your-resend-key SENDING_FROM_EMAIL=Mahalle <noreply@mahalle.digital> CONTACT_IP_SALT=your-32-char-random-secret ALLOWED_ORIGINS=https://mahalle.digital # optional: SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS (SMTP transport for local dev — wins over Resend when set) # optional: SENTRY_DSN / PUBLIC_SENTRY_DSN / SENTRY_AUTH_TOKEN / SENTRY_ORG / SENTRY_PROJECT (error monitoring) -
Run development server:
pnpm dev
The app will be available at
http://localhost:4321
pnpm dev- Start development serverpnpm build- Build for productionpnpm preview- Preview production buildpnpm type-check- Run TypeScript type checking
The app is configured for Vercel deployment using @astrojs/vercel adapter:
- Build command:
pnpm run build - Output directory:
.vercel/output - Runtime: Node.js 22
Vercel will automatically:
- Build the Astro app
- Deploy serverless functions (API routes)
- Handle SSR pages
- Run cron jobs defined in
vercel.json: daily news fetch (6:00 UTC) + account-deletion pipeline (5:30 UTC)
Scheduled jobs outside Vercel (GitHub Actions): air-quality logger (every 30 min, hits /api/cron/log-air), statistics sync (2×/year), nightly encrypted DB backup (db-backup.yml — mongodump → AES-256 → release asset in a private repo, 90-day rolling retention; see docs/runbooks/db-backup.md).
Production (mahalle) and development (mahalle-dev) are separate databases on the same Atlas cluster — the name rides in the MONGODB_URI path. Local dev and Vercel previews use mahalle-dev; seed it with pnpm tsx scripts/seed-dev-db.ts (refuses to run against any db whose name lacks "dev", prints a random per-run password for the fake accounts).
pnpm tsx scripts/create-auth-indexes.ts— idempotent. Ensures the auth/token/rate-limit indexes plus the two partial unique indexes onusers(users_handle_unique,users_email_unique— seedocs/runbooks/users-email-unique-index.md).pnpm tsx scripts/create-listing-indexes.ts— idempotent. Creates the marketplace partial indexes (listings.lastBumpedAt,listings.bundleId) + thelistingContactsrate-limit indexes + thelistingAuditTrailhistory index. Re-run is a no-op.pnpm tsx scripts/migrate-legacy-categories.ts --dry-runthenpnpm tsx scripts/migrate-legacy-categories.ts— one-time backfill that maps pre-kiosk English category keys (furniture/electronics/etc.) → the 13 German kiosk taxonomy keys + defaults missingdeliveryto'abholung'+ defaults missingmoderationStatusto'approved'. Idempotent.
- Hybrid Rendering: SSG for static pages, SSR for dynamic content
- Type Safety: Full TypeScript with Zod validation
- State Management: Local
useStatefor UI, TanStack Query for server state (with 24h localStorage persistence) - MongoDB Integration: Type-safe database operations (direct driver)
- NextAuth Authentication: Credentials provider with bcrypt + JWT strategy
- Responsive Design: Mobile-first approach
- Performance: Optimized with Astro's island architecture
- Content Moderation: Multi-layer AI moderation (safety scan + GPT content check for spam/hate speech/harassment) + trilingual profanity filters (TR/EN/DE) with leetspeak detection + username validation at registration + community reporting
- Admin moderation back-office (kiosk): card-based review queue with urgent-first sorting + author strike dots, Protokoll history table, decision modals replacing browser prompts — incl. the Ban-Bremse (third-strike rejections require an explicit ban confirmation with the author's strike ledger inline) and a bulk consequence preview (per-author strike deltas + ban acknowledgment before any bulk reject) — plus a mobile triage mode
- 3-strike ban enforcement: banned accounts cannot log in (kiosk „Konto gesperrt" card, enumeration-safe) and existing sessions turn read-only (26 write APIs return 403, non-dismissible suspended banner, compose pages redirect) — bans only ever happen through the confirmed moderation flows
- Daily Posting Limits: 5 per rolling 24h for topics, events, announcements, recommendations, and listings
- Newsboard: AI-curated local news from 9 RSS feeds + NewsData.io, with GPT-4o relevance scoring
- Marketplace (kiosk): 3 listing kinds (verkaufen / tausch / verschenken), 13 kiosk taxonomy categories with one-time backfill of legacy English-key listings (
scripts/migrate-legacy-categories.ts), delivery enum (Abholung / Versand / Abholung & Versand), optional detail fields (5 German free-text fields + condition enum), editorial lead-of-the-day on page 1, contact-form relay via Resend (privacy-preserving — no email addresses exposed), single-threshold 21d visibility: past-21d listings hide from public feed/search/direct URL (friendly "nicht mehr verfügbar" page at the same URL — HTTP 200, indexable-but-empty); author still sees them in „Meine Anzeigen" as grayed cards with a bump prompt, no bump rate limit (bump = freshness reset, available subject only to status/moderation guards), owner lifecycle (edit / bump / reserve / sold / delete with state-aware gating viacanMutateListing). Warning-labeled AND rejected listings are editable — the edit endpoint re-runs full moderation + writes a pre-edit snapshot tolistingAuditTrailfor provability. Mobile FAB for new listings, SEO-friendly hybrid SSR-static + island-hydrate detail pages. - Custom UI Dialogs: Native
<dialog>-based confirm modals and sonner toasts replace all browser-native dialogs - Kiez Data Dashboard: Interactive Schillerkiez neighborhood statistics with hand-drawn SVG charts, historical trends (demographics + social indicators 2013–2023), and live air quality data
- Public landing + login gating (Aug 2026): editorial one-pager at
/(masthead, KiezHeartbeat strip with server-side zero rule, blog/Kiez-Daten/Kurier teasers, one CTA, watercolor riso overprint background) — member surfaces (forum, calendar, newsboard, bookmarks, search, profiles) require login via a central middleware gate with?redirect=round-trip (URL-normalization open-redirect guard insrc/lib/auth/safeRedirect.ts); marketplace, blog, and Kiez-Daten stay public. Legal pages at/impressum+/datenschutz. - Forum (kiosk): Multi-collection merged feed (discussions + announcements + recommendations) on
/forumwith per-kind detail routes, per-kind card straps + chips, card height convergence (line-clamp-3body +min-h-[340px]), and resilientPromise.allSettledfetch (single-collection outage degrades to empty array for that kind only). - Official admin announcements: 7-day pinned slot at the top of the forum feed, server-enforced single-pin invariant via atomic displacement, admin dashboard at
/admin/announcementsfor create/edit/pin/unpin/delete. Admin role bypasses AI moderation. - Moderation visibility (forum + calendar): Author-only banners (
OwnStatusBannerfor pending / reported / rejected, with rejection-reason blockquote), author-only ghosting (dashedborder-warn/border-plum/border-danger+ body opacity), non-author "⚑ GEMELDET" chip for community-reported pending (no banner, no ghost — anti-stigma). Rejected items sort to the top of the author's view. Edit lockout (403 'edit_blocked_by_moderation') on any non-approved status — UI mirrors with visibly disabled edit buttons. - Calendar (kiosk): Live
nowticker store (60s aligned to wall-clock minute) drives "is this event live right now?" reactivity across detail modal, agenda, sidebar, month grid, and mobile day view. Saved events with optimistic mutations. Public attendee-profile lookup endpoint for the going-list stack. Dedicated edit page at/events/edit/[id]with flash-redirect cache-bust. - Profile (kiosk) & account lifecycle: own-profile Meldebogen with a derived Kiez-Chronik tenure timeline, cross-surface Archiv activity feed, printable A6 Steckbrief card (QR-coded), and public neighbor profiles at
/nachbarn/[handle](trimmed view, no e-mail/moderation/settings) reached by clicking any author byline. Self-service e-mail change (double-confirm, old session stays valid) and password change (other-device sign-out via apasswordChangedAt-vs-loginAtJWT check, silent same-device re-login). Account deletion with a 7-day undo grace period (in-app "Widerrufen" + mailed token) followed by an automated day-7 anonymization pipeline (Vercel cron): the account is tombstoned as "Ehemaliges Mitglied" while authored content is kept intact (Nachweispflicht) and the user's own RSVPs/bookmarks/listings are cleaned up. - Onboarding tour („Die Führung"): spotlight tour in seven per-surface chapters (Forum → Kalender → Marktplatz → Kurier → Kiez-Daten → Blog → Profil, 32 stops total) with ochre chrome, per-chapter seen-stamps on the user doc, and a „Hallo Kiez" post template handoff at the forum chapter's end
- Error monitoring: Sentry (errors-only, EU region, GDPR-conscious: no PII, no replay/tracing) with silent-degradation alerts for swallowed provider failures and an admin errors widget on
/admin/moderation - Forum post images: Up to 5 images per post (topics, announcements, recommendations) with Cloudinary upload, GPT-4o vision moderation, and scroll-snap carousel with arrow nav in the detail modal.
- Notification center (Aug 2026, R1 + R2): bell + panel in the kiosk nav — neighbors get notified about replies to their posts, moderation decisions (incl. strike number), official announcements (broadcast), and marketplace inquiries. Fan-out on write into a
notificationscollection (90d TTL), read-time actor-name join (deletion-safe), copy rendered client-side from i18n (locale toggle works retroactively), 90s visible-tab polling — plus optional web push (PWA — install Mahalle to the home screen on iOS) via a push-only service worker, no offline caching. - Forum bookmarks: Save/bookmark posts with server-side persistence (
savedPostscollection) and optimistic UI updates. Same pattern for saved events (savedEvents). - Forum search & tag filtering: Client-side filtering by title, body, author name, and tags. Clickable tag pills set the search value.
- Splash screen: One-per-session logo video intro (compressed to ~56 KB H.264). Skips on sub-pages, reduced-motion users, and subsequent visits. Dual-gate dismiss (video end +
window.load) with 4s safety timeout and autoplay-blocked fallback for mobile Firefox. - Performance: Cloudinary
f_auto,q_autoURL rewriter (src/utils/cloudinary.ts) applied to all user-uploaded images, SSR prefetch for forum default tab, batched$inauthor lookups, and localStorage-persisted React Query cache for instant page switches.
The app includes a comprehensive content moderation system:
- Layer 1 — Profanity filters: Turkish, English, and German blocklists with leetspeak normalization (catches obfuscated profanity like "m0therfu5ker5", "sh1tface", "a$$hole")
- Layer 2 — Safety scan: OpenAI
omni-moderation-latestscans all content types on submission (topics, comments, events, announcements, recommendations, marketplace listings) - Layer 3 — GPT content check:
checkSpamWithGPT()catches spam, ads, scams, hate speech, and harassment that the safety scan misses — runs on all content types. Hate speech and harassment are flagged as urgent. - Layer 4 — Image safety (marketplace + forum posts): GPT-4o vision scans images for inappropriate content
- Layer 5 — Username validation:
checkNameProfanity()at registration runs all blocklists (word-boundary + substring for concatenated profanity) plus OpenAI Moderation API + GPT hate speech check - All checks run in parallel via
Promise.all()and are merged withmergeModerationResults() - Content exceeding thresholds is queued for admin review
- Fail-safe: If any API fails, content is queued for manual review (never auto-approves on error)
- Daily posting limits: 5 per rolling 24h for topics, events, announcements, recommendations, and listings. Comments excluded.
- Users can report all content types via 🚩 flag button
- Report reasons: spam, harassment, hate speech, violence, inappropriate, misinformation
- Duplicate reports tracked per user (prevents spam reporting)
- Reported content stays visible but locked from editing/deleting
- Author sees orange banner: "Your content has been reported by the community"
- Queue view: Review pending flagged content with row selection and bulk actions
- History view: Sortable table with column visibility toggle and human-readable category labels
- Filter tabs: All, Discussions, Comments, Announcements, Events, Recommendations, Marketplace
- Stats counters: Urgent, Pending, Approved, With Warning, Rejected
- Actions:
- ✓ Approve (publish content)
- ⚠ Approve with Warning (add content warning label)
- ✕ Reject (remove content, add strike to author)
- Bulk Approve / Bulk Reject (up to 50 items at once)
- Strike system: 3 strikes = automatic user ban
| Status | Visible to Others | Author Sees | Edit/Delete |
|---|---|---|---|
| AI flagged (pending) | ❌ | Amber "under review" banner | ❌ Disabled |
| User reported (pending) | ✅ | Orange "reported" banner | ❌ Disabled |
| Approved | ✅ | Normal | ✅ Enabled |
| Approved with Warning | ✅ (blurred until revealed) | Warning badge | ✅ Enabled |
| Rejected | ❌ | Red "rejected" banner | ❌ Disabled |
The app includes an AI-powered local news aggregation system:
- Vercel cron job runs daily at 6 AM UTC, triggering
/api/news/fetch-daily - Fetches from 9 RSS feeds (Tagesspiegel, Berliner Zeitung, Berliner Kurier, nd-aktuell, taz, Kiez und Kneipe, Schillerpromenade, Facetten Neukölln, Pro Schillerkiez) + NewsData.io API
- GPT-4o relevance scoring: Each article scored 0-100 for Berlin/Neukölln relevance
- Only articles scoring ≥70 are saved (max 20/day)
- GPT also generates 2-3 sentence German summaries for paywalled articles
- AI-fetched articles: Auto-approved, no moderation needed
- User-submitted articles: Go through the standard moderation pipeline
- Articles without images use
og:imagescraping as fallback, then a local placeholder
- Date filter tabs: 7 Days, 30 Days, 3 Months, 6 Months, 1 Year, Archive
- Live search with 300ms debounce
- Pagination with page size selector and First/Prev/Next/Last navigation
- Modal view with keyboard navigation (← →)
- Server-side bookmark persistence for logged-in users
The /schillerkiez page shows neighborhood-level statistics for the Schillerkiez area in Berlin-Neukölln.
- Demographics (half-yearly): Population, age distribution, migration background, gender — from Amt für Statistik Berlin-Brandenburg
- Social indicators (biennial, 2013–2023): Unemployment rate, child poverty, transfer benefits, Status/Dynamik index — from Monitoring Soziale Stadtentwicklung Berlin
- Air quality (live): PM10, NO₂, O₃, CO grades from BLUME station MC042 (Nansenstraße)
scripts/sync-stats.tsdownloads XLSX files, parses with ExcelJS, and upserts to MongoDBscripts/backfill-history.sh— one-time demographic backfill (6 periods)scripts/backfill-social.sh— one-time MSS social index backfill (2013–2021)- GitHub Actions workflow runs 2x/year (March + September) + manual dispatch
- Handles Berlin's 2021 LOR reform: auto-detects old (2 PLR) vs new (4 PLR) area codes
- Air Quality — live pollutant grades with color-coded scale
- Age Distribution — horizontal bar charts (aggregate + per-PLR carousel)
- Migration Background — donut charts showing non-overlapping segments
- Gender — donut charts (male/female split)
- Social Snapshot — horizontal bar charts for unemployment, child poverty, transfer benefits
- Population Trend — line charts (aggregate + per-PLR + migration diversity %)
- Social Trend — "Soziale Entwicklung" carousel showing 10-year trends with merged old/new LOR lines
- Sources — data attribution, LOR reform explanation, index definitions
STATS_XLSX_URL/STATS_PERIOD— AfS demographics XLSXMSS_XLSX_URL/MSS_PERIOD— MSS social index XLSXMSS_SDI_URL— MSS Status/Dynamik index XLSX (optional)
POST /api/auth/register- User registration
GET /api/topics- List topicsPOST /api/topics/create- Create topicPUT /api/topics/edit/[id]- Edit topicDELETE /api/topics/delete/[id]- Delete topic
GET /api/events- List events (with date range filter)POST /api/events/create- Create eventPUT /api/events/edit/[id]- Edit eventDELETE /api/events/delete/[id]- Delete event- Date range selection: Click future days in the calendar grid to select a range (teal highlight), extend/shorten by clicking after/within the range, then click the floating "+" tooltip to open the event form with dates pre-filled
GET /api/announcements- List announcementsPOST /api/announcements/create- Create announcementPUT /api/announcements/edit/[id]- Edit announcementDELETE /api/announcements/delete/[id]- Delete announcement
GET /api/recommendations- List recommendationsPOST /api/recommendations/create- Create recommendationPUT /api/recommendations/edit/[id]- Edit recommendationDELETE /api/recommendations/delete/[id]- Delete recommendation
GET /api/comments/[postId]- Get comments for a postPOST /api/comments/create- Create commentDELETE /api/comments/delete/[commentId]- Delete comment
POST /api/likes/toggle- Toggle like on contentPOST /api/views/increment- Increment view count
GET /api/news- List news (with pagination, search, date filters)POST /api/news/submit- Submit user news articleGET /api/news/fetch-daily- Trigger daily AI news fetch (cron, requires CRON_SECRET)POST /api/news/save- Save/unsave a news article (bookmark)GET /api/news/save- Get user's saved news IDsGET /api/news/preview- Preview metadata from a URL
GET /api/listings- Browse listings (with filters)POST /api/listings/create- Create listingPUT /api/listings/edit/[id]- Edit listingDELETE /api/listings/delete/[id]- Delete listingGET /api/listings/daily-count- Get user's daily listing countPOST /api/listings/draft- Save/update draft listing (relaxed validation, no moderation)POST /api/listings/draft/[id]/publish- Publish draft (full moderation + daily limit check)POST /api/listings/[id]/bump- Bump listing to reset its 21-day freshness clock + sort to top (no rate limit; gated only on status/moderation; supersedes the original A5 7-day cooldown)POST /api/listings/[id]/status- Update listing status (available / reserved / sold / exchanged)POST /api/listings/[id]/contact- Send buyer→seller contact message (Resend relay, no email exposure)
POST /api/reports/submit- Submit user reportGET /api/admin/moderation- List flagged content (admin)POST /api/admin/moderation/review- Approve/reject content (admin)POST /api/admin/moderation/bulk-review- Bulk approve/reject up to 50 items (admin)
GET /api/kiez-stats- Schillerkiez demographics, social indicators, and trends (public, 24h cache)GET /api/kiez-air- Live BLUME air quality grades for station MC042 (public, 30 min cache)GET /api/kiez-heartbeat- Aggregate landing-strip data (air / posts this week / weekend events / today's Kurier issue), public, 1h in-DB cache, zero rule applied server-side
POST /api/upload/image- Upload image to Cloudinary (profile pictures)POST /api/posts/upload- Upload forum post image to Cloudinary (max 5MB)POST /api/posts/save- Toggle save/unsave forum post bookmarkGET /api/posts/save- Get user's saved post IDsPOST /api/users/update- Update user profile
Required environment variables:
AUTH_SECRET- NextAuth secret keyAUTH_TRUST_HOST- Set totruefor Vercel deploymentNEXTAUTH_URL- Canonical app origin (https://mahalle.digital) — required in prod; emailed links (password reset, e-mail verification) are built from it and the forgot-password flow fails closed without itMONGODB_URI- MongoDB connection string (db name in the URI path:/mahalleprod,/mahalle-devdev/preview)CLOUDINARY_CLOUD_NAME- Cloudinary cloud nameCLOUDINARY_API_KEY- Cloudinary API keyCLOUDINARY_API_SECRET- Cloudinary API secretOPENAI_API_KEY- OpenAI API key (content moderation + news relevance scoring)CRON_SECRET- Vercel cron job authentication secretNEWSDATA_API_KEY- NewsData.io API key (optional, for additional news sources)RESEND_API_KEY- Resend.com API key — production email transport (auth mails + contact relay)SENDING_FROM_EMAIL- Sender address for all app email, e.g.Mahalle <noreply@mahalle.digital>SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS- Optional SMTP transport (local dev; wins over Resend when set)CONTACT_IP_SALT- 32+ char secret, fixed across deploys, used to hash IPs in rate-limit keysALLOWED_ORIGINS- CSV of allowed origins for contact relay + resend-verification CSRF guard (e.g.https://mahalle.digital)SENTRY_DSN/PUBLIC_SENTRY_DSN/SENTRY_AUTH_TOKEN/SENTRY_ORG/SENTRY_PROJECT- Error monitoring (all optional; init no-ops without the DSNs)STATS_XLSX_URL- AfS demographics XLSX URL (optional, sync script)STATS_PERIOD- AfS period, e.g. "2025h2" (optional, sync script)MSS_XLSX_URL- MSS social index XLSX URL (optional, sync script)MSS_PERIOD- MSS report period, e.g. "2023" (optional, sync script)MSS_SDI_URL- MSS SDI XLSX URL (optional, sync script)MSS_BEZIRKE_XLSX_URL- MSS Bezirke-level XLSX for the Berlin-Vergleich reference import (optional, sync script)VAPID_PRIVATE_KEY- Web push VAPID private key, server-secret (optional; unset ⇒ push no-ops silently)PUBLIC_VAPID_PUBLIC_KEY- Web push VAPID public key, ships to the client bundle (optional, pairs withVAPID_PRIVATE_KEY)
-
Check MongoDB connection:
- Verify
MONGODB_URIin.env - Check MongoDB Atlas network access
- Verify
-
Vercel deployment issues:
- Check build logs in Vercel dashboard
- Verify environment variables are set in Vercel project settings
-
TypeScript errors:
- Run
pnpm type-check - Check
tsconfig.jsonconfiguration
- Run
For more information:
- Astro Documentation
- TanStack Query
- Motion Documentation
- auth-astro
- MongoDB Node Driver
- Vercel Deployment
© Ercan Atak — Mahalle. Licensed under PolyForm Noncommercial 1.0.0 with the Mahalle Field-of-Use Rider — see LICENSE.
Free for noncommercial use by neighborhood associations, housing cooperatives, public institutions, and community groups. Not permitted for political parties, for-profit entities, religious proselytizing, single-individual personal deployments, or surveillance of residents. Commercial licensing inquiries: contact the author.
Before deploying or forking, please read MANIFESTO.md — the project's binding statement of intent on what Mahalle is, who it is for, and what it refuses to become.