An invite-only community platform built on trust, belonging, and genuine human connection. Every account represents someone you've met face-to-face.
Our Place is a community platform built on one conviction: online spaces should be rooted in real-world relationships. It brings the analog back into the digital — every account begins with a face-to-face connection, and everything the platform does is meant to push interaction back out into the real world.
There is no public registration. Accounts are created in person by existing members who have met you face-to-face. This "web of trust" means every person here is a real human, vouched for by someone in the community — and it's how Our Place grows beyond its first members.
Most social media silos people into echo chambers and infinite scroll. Our Place is the opposite: a digital layer for real communities, where you keep up with what's happening locally, start and share events, and turn online conversations into in-person ones.
- The forum — a Reddit-/Discord-inspired space to follow the communities you care about: post, comment, react, and organize events. Familiar social media, without the dark patterns.
- The world — an 8-bit RPG overworld you can teleport into, where each community is a building in a town. Instead of scrolling mindlessly, you wander, explore, and build — leaving room for the kind of boredom that turns into a creative idea. Approach a building and step inside to reach its forum content. Think Roblox meets Reddit, but pixel art.
The forum is fully functional today. The world is actively in development.
- Social Media That's Actually Social — The goal isn't time-on-app, it's getting people offline and together. Digital interaction here is a means to real-world connection, not a replacement for it.
- In-Person First — Accounts are created face-to-face by an admin or trusted member. No anonymous sign-ups, no bots, no strangers. Every user is someone a real person has met and vouched for.
- Web of Trust — The community grows organically through real relationships. You can trace every account back to a chain of people who know each other.
- Your Algorithm, Your Rules — Users will control their own feed algorithm. No engagement-maximizing dark patterns, no infinite dopamine loops. You decide what you see.
- Physical Third Spaces — The long-term vision includes physical community spaces (coffee shops, coworking hubs) where Our Place serves as the digital layer for a real neighborhood.
- Communities — Create or join communities organized by category (Gaming, Creative, Tech, etc.)
- Rich Posts — Text, photo, video, and rich editor post types
- Comments & Reactions — Threaded comments and emoji reactions on posts
- Events — Community event creation and management
- Feed — Personalized feed with explore and friends tabs
- My Place — Personal profile space for each user
- File Uploads — Image and media uploads with validation
- Invite-only accounts — admin-only account creation via dashboard (
/admin) - JWT auth with httpOnly cookies and bcrypt password hashing
- Password reset flow
- Rate limiting on all auth and content creation routes
- Zod schema validation on all API request bodies
- Role-based access control (admin/user roles)
- Account settings — update your email, phone, and password (current password required)
Three hand-built retro themes, switchable any time in profile → Account → Appearance:
- Platinum — System 7 / classic-Mac chrome: pinstriped window cards, 1-bit hard shadows, a dithered desktop, and a pixel wordmark.
- Terminal — dark phosphor: monospace body text,
$-prompt headings with a blinking cursor, and faint CRT scanlines. - Pixel Dusk — warm paper, chunky plum RPG-dialog borders, hard offset shadows, and buttons that press down when you click them.
The default, Auto, follows the clock — Platinum by day, Terminal at night — so the place looks different depending on when you visit. Your choice saves to your account and follows you across devices.
An isometric 2.5D overworld you teleport into:
- Isometric engine — React + HTML Canvas, a 2:1 diamond projection with an autotiled ground, depth-sorted free-standing objects, and an 8-direction animated character
- Player movement (WASD/arrows + mobile touch D-pad), a camera that follows and clamps to the map, and per-tile collision
- Ports — walk up to a building's door and step through to its community's forum view; Portal buttons drop you back at its doorstep
- Mushroom warp network — discover shrines to unlock fast travel between them
- The Capital — an authored starter town with a building (and a Ports door) for each community
- Avatar builder — gender-neutral character customization on first login
The same My Place profile rendered in each built-in theme. Auto mode switches between Platinum and Terminal with the time of day.
| Platinum System 7 · day |
Terminal phosphor · night |
Pixel Dusk warm paper · opt-in |
![]() |
![]() |
Communities — discover and join spaces organized by category.
Inside a community — posts, threaded comments, and emoji reactions.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| ORM | Prisma 7 |
| Database | PostgreSQL |
| Styling | Tailwind CSS |
| Auth | JWT + bcrypt |
| Validation | Zod |
| Testing | Vitest (unit), Playwright (UI) |
src/
├── app/
│ ├── api/ # REST API routes
│ │ ├── admin/ # Admin dashboard API (user management)
│ │ ├── auth/ # Login, password reset
│ │ ├── communities/# CRUD, join/leave, posts
│ │ ├── posts/ # Comments, reactions
│ │ ├── feed/ # Personalized, explore, friends
│ │ ├── my-place/ # Personal space posts
│ │ ├── events/ # Community events
│ │ └── upload/ # File uploads
│ ├── admin/ # Admin dashboard (account creation)
│ ├── auth/ # Auth pages (login, password reset)
│ ├── communities/ # Community browsing and detail pages
│ ├── feed/ # Feed dashboard
│ ├── world/ # 8-bit overworld page
│ └── profile/ # User profile
├── components/ # Reusable React components
│ ├── WorldCanvas.tsx # Game engine canvas component
│ ├── PostCard.tsx # Post display
│ ├── Navbar.tsx # Navigation bar
│ └── ...
├── generated/prisma/ # Auto-generated Prisma client (not committed)
└── lib/
├── game/ # Game engine (sprites, input, engine, types, tileset)
├── types/ # TypeScript type definitions
├── db.ts # Prisma client singleton
├── schemas.ts # Zod validation schemas
├── pagination.ts # Pagination utilities
└── media-utils.ts # File upload helpers
prisma/
├── schema.prisma # Database schema (source of truth)
├── migrations/ # Prisma migration history
└── seed.ts # Seed data (9 starter communities)
scripts/
├── generate-world.ts # Procedural frontier world generator (deterministic)
├── generate-tiles.lua # Aseprite script — generate tile sprite sheet
└── generate-player.lua # Aseprite script — generate player sprite sheet
public/world/
├── world.bin # Generated tile grid (500×500, one byte per tile)
└── world.meta.json # Spawn, doors, node bounds, passages, mushroom network
- Node.js 20+
- npm
- PostgreSQL (local or hosted)
git clone https://github.com/luke-whitaker/our-place.git
cd our-place
npm install- Create a PostgreSQL database (locally or on a service like Railway)
- Copy
.env.exampleto.env.localand set yourDATABASE_URL - Run migrations and seed:
npx prisma migrate dev # Apply schema migrations
npm run db:seed # Seed starter communitiesnpm run devOpen http://localhost:3000.
| Command | Purpose |
|---|---|
npm run build |
Production build |
npm run lint |
ESLint |
npm run format |
Prettier auto-fix |
npm run test |
Run unit tests |
npm run test:watch |
Run tests in watch mode |
npm run db:migrate |
Run Prisma migrations |
npm run db:seed |
Seed starter communities |
npm run db:studio |
Open Prisma Studio (DB viewer) |
- Core forum platform (communities, posts, comments, reactions)
- Invite-only auth with admin dashboard
- Rich post types and file uploads
- Feed system with explore/friends tabs
- My Place personal profiles
- Security hardening (rate limits, Zod validation, transactions)
- Game engine foundation (canvas, movement, camera, interactions)
- PostgreSQL + Prisma migration (see v0.2.0 below)
- Deploy to Railway (PostgreSQL + Dockerfile — see v0.3.0 below)
- 32px tile upgrade + Aseprite generation pipeline
- Avatar builder (gender-neutral first-login customization)
- Procedural frontier world generator (500×500, 6 biomes, capital stamp, mushroom network)
- Wire generated world into
WorldCanvas(Phase B: loader + renderer) - Mushroom warp UI (warp menu, discovery tracking, teleport transition)
- Ports v1 — two-way travel between forum view and world view (Portal buttons + doors)
- Isometric world — 2.5D engine (iso projection, autotiled ground, depth-sorted objects, 8-direction character) with collision, doors, warp shrines, region toasts, and Ports
- Authored capital town — a building with a Ports door for each community, composed as a serializable world document
- Distinct building art — each community building is one of the six Evergrow Town_House sprites, half-scaled to fit the town's lots, with collision matched to each base
- Water autotiling + richer terrain
- Ports v2 — building interiors with PC sprites (log on to the forum, or warp PC-to-PC)
- DB-backed worlds + Builder/Creator placement (user-built buildings and objects)
- Player identity bound to world position + username rendered above the avatar
- Real-time multiplayer presence (the engine is built with the seams for it)
Why: The world had been a top-down tile map — functional, but flat. To make it the explorable, characterful place the project is about, it moved to an isometric 2.5D view built around a purchased character and the Evergrow Forest art. The migration was also the moment to lay architecture seams for where the world is headed: a shared, multiplayer, player-built space.
What changed:
- Isometric engine — a 2:1 projection with a diamond-autotiled ground, depth-sorted free-standing objects (trees, rocks, buildings, the warp shrine), and an 8-direction animated character. Movement, collision, doors, warp shrines, region toasts, the warp menu, and fade transitions all carried over from the top-down engine, now in iso.
/worldruns on it; the top-down engine, tileset, sprite generators, and the 500×500 procedural generator were retired. - Serializable world model — a place is a plain
IsoWorlddocument (terrain grid + a list of placed objects + doors + shrines + regions) in world-space tile coordinates, validated with Zod and loaded behind a source-agnostic interface (a file today, a database row later). Collision is a pure function over that data — the same code a server could run. - The Capital — an authored starter town: streets and a central plaza, a building for each community (whose door ports you into that community's forum view), framing trees, and the mushroom warp network. Buildings use a placeholder cottage for now; swapping in distinct art is a per-building one-liner.
- Built for what's next — the engine models the world as a static map plus an entity collection (the local player is one entity), splits input (
computeIntent) from movement (applyMovement), and keeps positions in world space. Those are the seams that let parallel and real-time multiplayer — and Builder/Creator user-generated spaces — slot in later without a rewrite.
What didn't change: the forum, Ports' contract (/world?at=<slug> ↔ a door porting back to /communities/<slug>), and the avatar builder's procedural preview. The account model and APIs are untouched.
Not yet done (intentional): distinct per-building art (the Evergrow Town_House sprites), water autotiling, Ports v2 interiors, and binding player position to identity in the database.
Why: The forum had been deliberately unstyled while the platform went live. This cycle gave Our Place its look — and instead of picking one retro direction, all three became user-selectable themes. The default, Auto, follows the clock: the place looks different at night, like a real place does.
What changed:
- Semantic design tokens — every component now references a single palette
(
surface/ink/line/accent) defined inglobals.cssvia Tailwind v4@theme; retheming is a values-edit in one file. - Three themes —
Platinum (System 7 chrome: pinstriped window cards, 1-bit hard shadows, dithered
desktop, pixel wordmark), Terminal (dark phosphor: monospace body,
$-prompt headings with a blinking cursor, faint CRT scanlines, flat panels), and Pixel Dusk (warm paper, chunky plum RPG-dialog borders, hard offset shadows, buttons that press down, amber wordmark). - Auto mode — Platinum 7am–7pm, Terminal at night; any theme can be pinned in
profile → Account → Appearance (live preview swatches). The choice is saved to the
account (
users.theme) and follows you across devices, with a localStorage echo and a CSP-nonce'd pre-paint script so the right theme renders with no flash. - Theme typography — Pixelify Sans display headings (Platinum/Dusk), VT323 + IBM Plex
Mono (Terminal), loaded via
next/font.
What didn't change: layout, components, and API shapes. Themes are token values plus a thin chrome layer — no component was redesigned.
Why: Uploads were written to a persistent volume on the host, which tied media to a single deploy environment, required the container to start as root to fix mount ownership, and left bandwidth egress as the looming cost driver. Object storage with zero egress fees is the right long-term home for media.
What changed:
- Uploads go to Cloudflare R2 — the upload API now does a signed PUT to R2
(
src/lib/storage.ts, via the dependency-freeaws4fetch) and returns the absolute public URL. Object keys mirror the old/uploads/<type>/<uuid>.<ext>layout. - CSP follows the media — the R2 public host is added to
img-src/media-srcinsrc/proxy.ts, derived fromR2_PUBLIC_BASE_URLat runtime. - Volume teardown — with no runtime writes to
public/, the Dockerfile now runs as thenextjsuser from the start (USERdirective); the root-start +su-execprivilege drop and thestart.shownership fixups are gone, along with the volume itself.
What didn't change: API response shapes, accepted file types, and size limits are identical.
Why: The generated frontier had been sitting on disk since April. This cycle made it the actual, explorable heart of the platform — and introduced Ports: the idea that the forum and the world are two views of the same place, and you travel between them deliberately.
What changed:
- World loader —
WorldCanvasnow fetches the generated 500×500 frontier (world.bin+world.meta.json) and renders it through the existing frustum-culled engine. The hand-built test map is retired from the live page. - Ports (v1) — "Portal" buttons on My Place and community pages drop you into the world
at that building's door (
/world?at=<slug>); walking into a door ports you back to that place's forum view. Two views, one place. - Mushroom fast travel — walking up to a shrine discovers it ("Sun Beach Shrine discovered!"); pressing Enter opens the Mycelium Network warp menu listing your discovered shrines. The Capital Gate starts unlocked. Warps ride the existing fade transition.
- Region toasts — entering the capital or any of the 6 frontier nodes shows a brief banner with the region's name.
- Per-device persistence — position and discovered shrines are saved to localStorage (a stopgap until player position is bound to identity in the DB).
- Account settings — profile → Account now edits email, phone, and password (current password required, rate-limited).
- Production fixes — media rendering after the Prisma migration (video embeds, photo galleries), upload permissions on the Railway volume, date parsing fossils.
Next (Ports v2): building interiors with PC sprites — enter a building, sit at the PC, and choose to "log on" (exit to the forum view) or warp to another PC.
Why: With the platform on Postgres and the forum stable, this cycle focused on three things: getting Our Place actually running in production, making the first-login experience feel personal, and laying the groundwork for the 8-bit world to be more than a bare test map.
What changed:
- Deployed to Railway — production Dockerfile (multi-stage build with standalone Next.js output), PostgreSQL service linked, healthcheck on
/. Several iterations to get the Docker runner stage correct: fullnode_modulescopy (native binaries + Prisma/effect runtime deps), Prisma schema copied into deps stage, dummy env vars for build-time Next.js compilation,.npmrcremoved so native binary installs work. - Avatar builder — gender-neutral character customization shown on first login. Hair style, skin tone, shirt color, pants color, stored as JSON on the user record. No male/female selector.
- Audit overhaul — invite-only auth tightened, admin dashboard cleanup,
createUserSchemaconsolidation (removed orphanedregisterSchema), code-quality pass across the admin surface. - 32px tile upgrade — tile size doubled from 16px to 32px for better readability at modern resolutions. New Aseprite Lua scripts (
scripts/generate-tiles.lua,scripts/generate-player.lua) for sprite-sheet generation. - Procedural frontier world generator (
scripts/generate-world.ts,npm run world:generate) — deterministic 500×500 tile world built from a single seed. 9-stage pipeline: base fill → 8 passages (tree-walled corridors with tall-grass patches) → lakes + rivers (Iowa River N-S) → 6 themed nodes (flower meadow, beach, mountain valley, island, misty grove, ancient ruins) → capital stamp at (220,230) → wilderness fill (noise-driven forest vs. clearing) → border wall → mushroom warp network (1 capital gate + 6 node shrines, full-mesh connections). Emitspublic/world/world.bin(one byte per tile) andworld.meta.json(spawn, doors, node bounds, passages, mushroom network). - 8 new tile types —
TALL_GRASS,FLOWER_RED/YELLOW/PURPLE,SAND,MOUNTAIN,MUSHROOM,STONE_RUIN— palette entries and placeholder procedural sprites (to be refined in Aseprite later). - Seed trimmed — 12 starter communities → 9, with simpler names.
Not yet done (intentional): The generated world is on disk but not yet read by WorldCanvas. Phase B — loader + renderer + mushroom warp UI — is the next cycle.
Why: SQLite (better-sqlite3) was the right choice for prototyping — zero setup, file-based, fast to iterate. But Our Place is a multi-user platform headed for production deployment. SQLite can't handle concurrent writes from multiple users reliably, and it doesn't work on most cloud hosting platforms (Railway, Render, etc.) without workarounds. PostgreSQL is the industry standard for this kind of app.
What changed:
- Database engine: SQLite (better-sqlite3) → PostgreSQL, using
@prisma/adapter-pgdriver - ORM: Raw SQL queries → Prisma 7 with full type-safe client
- Schema: Defined in
prisma/schema.prisma(single source of truth) instead of inlineCREATE TABLEstatements indb.ts - Migrations: Runtime column-checking hacks (
PRAGMA table_info) → Prisma's migration system (prisma migrate dev) - Seeding: Moved from
initializeDatabase()to a dedicatedprisma/seed.tsscript - All 18 API routes converted from synchronous
db.prepare().run/get/all()to async Prisma client calls - SQLite-specific syntax replaced:
datetime('now')→@default(now()),MAX(0, x)→GREATEST(0, x),COLLATE NOCASE→ Prisma'smode: "insensitive",INSERT OR IGNORE→upsert - Config updates: Removed
better-sqlite3from dependencies, updatednext.config.ts, added Prisma scripts topackage.json
What didn't change: All API response shapes are identical. The frontend is unaffected — no client-side code was modified.
Forum platform with full auth, communities, posts, comments, reactions, events, file uploads, feed, and "My Place" profiles. 8-bit game engine prototype with tile rendering, player movement, camera system, and building interactions. Built with Next.js 16, TypeScript, SQLite, and Tailwind CSS.
- Portfolio Site — My pixel-art RPG portfolio, the prototype that inspired the game engine in this project
Luke Whitaker — Linguist, researcher, and developer working at the intersection of language, technology, and digital interfaces.




