field journal platform

Setup & User Guide

Everything you need to deploy The Noticing Project, onboard your students, and run a term — for instructors, students, and developers.

Overview

This guide walks you through deploying The Noticing Project for the first time. Setup takes about 45–60 minutes. Once done, starting each new term takes about 5 minutes.

You will create free accounts at four services. None require a credit card for the usage levels typical of a 30-student term.

GitHub
github.com
Stores your code and triggers automatic deploys.
free
Vercel
vercel.com
Hosts the app. Deploys automatically on every code push.
free
Vercel Postgres
vercel.com/storage
Hosts the database — created in one click from the Vercel dashboard. No separate account needed.
free
Cloudinary
cloudinary.com
Stores photos and audio. Auto-compresses images on upload.
free

Step 1 — Put the code on GitHub

1
Download and unzip the project

Download the project zip from the repository releases page and unzip it on your computer.

2
Create a new GitHub repository

Go to github.com/new. Name it noticing-project. You can make it private if you prefer.

3
Push the code

From your terminal, inside the unzipped project folder:

git init git add . git commit -m "initial setup" git remote add origin https://github.com/YOUR-USERNAME/noticing-project.git git push -u origin main

Step 2 — Set up the database (Vercel Postgres)

The database is created directly inside Vercel — no separate account or CLI needed. You'll do this after deploying in Step 6, but it's listed here so you know what to expect.

💡

In Vercel, go to your project → StorageCreate DatabasePostgres. Vercel automatically adds the database credentials to your environment variables. That's it — nothing to install or configure manually.

Step 3 — Set up media storage (Cloudinary)

1
Note your credentials

Sign in at cloudinary.com. On your Dashboard, find your Cloud Name, API Key, and API Secret.

2
Create an upload preset

Go to Settings → Upload → Upload presets → Add upload preset. Set these three fields — everything else can stay at its default:

  • Preset name: noticing-project — must match exactly; the app references it by name.
  • Signing mode: set to Unsigned. This is required. It allows students' browsers to upload directly to Cloudinary without routing files through your server. If left on Signed, all uploads will fail with a permissions error.
  • Asset folder: noticing-project — keeps all project media in one folder in your Cloudinary library so it does not mix with anything else. Not strictly required, but strongly recommended.

Click Save when done.

💡

Why Unsigned? Students' browsers upload directly to Cloudinary — your server never handles binary file data. This keeps the app fast and hosting costs at zero. If uploads ever stop working, the first thing to check is that this preset is still set to Unsigned.

Step 4 — Configure environment variables

Find the file named .env.example in the project folder. You need to create two env files — Prisma reads .env, while Next.js reads .env.local. They serve different purposes.

💡

Database credentials are set automatically. When you create a Vercel Postgres database in Step 6, Vercel adds POSTGRES_PRISMA_URL and POSTGRES_URL_NON_POOLING to your environment variables automatically. You do not need to set these manually.

Create .env.local

Duplicate .env.example, rename the copy to .env.local, and fill in each value:

Then create a symlink so Prisma (which only reads .env) can see the same variables:

ln -s .env.local .env
💡

This means one file to maintain. Both .env and .env.local are gitignored so neither gets committed.

POSTGRES_PRISMA_URLSet automatically by Vercel when you create a Postgres database. Not needed in .env.local.
POSTGRES_URL_NON_POOLINGSet automatically by Vercel. Not needed in .env.local.
NEXTAUTH_SECRETA random string. Generate one: openssl rand -base64 32
NEXTAUTH_URLhttp://localhost:3000 for local testing
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAMEYour Cloudinary cloud name
CLOUDINARY_API_KEYYour Cloudinary API key
CLOUDINARY_API_SECRETYour Cloudinary API secret
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESETnoticing-project
GEOFENCE_POLYGONLeave the default Dartmouth value for now — adjust in Step 7

Install dependencies:

npm install
💡

The npm run db:push command is only needed after the Vercel Postgres database is created. Vercel runs prisma generate automatically during each deploy.

Step 5 — Create your admin account

Run this once from the project folder. Replace the values in quotes with your own details:

node -e " const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcryptjs'); const prisma = new PrismaClient(); async function main() { const hash = await bcrypt.hash('YOUR-PASSWORD', 12); await prisma.user.create({ data: { username: 'admin', email: 'you@dartmouth.edu', displayName: 'Instructor', passwordHash: hash, role: 'admin', }}); console.log('Admin created'); } main().finally(() => prisma.\$disconnect()); "
💡

This only needs to be run once, ever. Your admin account persists across all terms.

Step 6 — Deploy to Vercel

1
Import your repo

Go to vercel.com/new and click Import next to your noticing-project repository.

2
Add environment variables

Before clicking Deploy, open the Environment Variables section and add the variables from .env.local. Set NEXTAUTH_URL to your Vercel domain, e.g. https://noticing-project.vercel.app. Do not add POSTGRES_PRISMA_URL or POSTGRES_URL_NON_POOLING yet — these are added automatically when you create the database.

3
Deploy

Click Deploy. Vercel builds and publishes the site in about 60 seconds.

4
Create the Postgres database

In your Vercel project, go to Storage → Create Database → Postgres. Follow the prompts — Vercel automatically adds POSTGRES_PRISMA_URL and POSTGRES_URL_NON_POOLING to your environment variables and triggers a redeploy.

5
Push the database schema

Once the Postgres database exists, run this once from your terminal to create all the tables:

npx vercel env pull .env.vercel DATABASE_URL=$(grep POSTGRES_PRISMA_URL .env.vercel | cut -d= -f2-) npx prisma db push

Then delete .env.vercel — it contains credentials and should not be committed.

Step 7 — Adjust the campus boundary

The app ships with a default Dartmouth polygon. To refine it:

1
Open the geofence editor

Sign in to your live site and go to /admin/geofence.

2
Trace the boundary

Click points on the map to outline the campus area you want to include. Use Undo last to correct mistakes.

3
Copy and save

Click Copy GeoJSON to clipboard. In Vercel, go to Settings → Environment Variables, update GEOFENCE_POLYGON, then Redeploy.

Each term: what to do

At the start of term

1
Create and activate a term

Go to /admin. Click Create term, name it (e.g. Winter 2026), then click Activate.

2
Add your students to the whitelist

In the Registration whitelist section, paste your class email list from your course management system — one email per line. Only whitelisted emails can create accounts.

3
Share the URL

Send students the site URL. They register, claim a spot, and begin posting.

At the end of term

Go to /admin and click Archive term. All journals become read-only and remain publicly visible indefinitely. Students keep their accounts and can re-enroll in future terms.

What is this?

The Noticing Project is a field journal platform. You will claim a single spot on the Dartmouth campus, return to it throughout the term, and record what you observe — in writing, photos, audio, and video.

We live in what scholars call an "attention economy" — an environment where our focus has become a resource that apps, ads, and platforms compete to capture. The rhythm of daily life tends toward speed: we glance rather than gaze, skim rather than read, and move on before we have fully arrived.

This project invites you to practice slow looking — returning again and again to one seemingly unremarkable place and documenting what reveals itself through patient observation. The goal is not to reject technology, but to cultivate a complementary skill: the ability to direct your attention deliberately and discover what emerges when you give a small corner of the world your sustained, unhurried gaze.

Creating your account

1
Go to the site and click Join

Your instructor will share the URL. Click Join in the top-right corner.

2
Register with your Dartmouth email

Use the exact email address your instructor has on file — registration is by invitation only. Choose a username that will appear publicly on the map and your journal.

3
You're in

After registering you'll be taken directly to the campus map to claim your spot.

💡

If registration is blocked, check that you're using the exact email your instructor enrolled — not an alias or forwarding address.

Claiming your spot

You get one spot per term. Choose carefully — you will be returning to it many times.

1
Click "+ claim a spot" in the sidebar

The map enters placement mode.

2
Click anywhere on campus to drop your pin

The pin must fall within the campus boundary. Click again to move it. Pins cannot be placed very close to another student's spot.

3
Name your spot precisely

Not just "a bench" but "the bench facing east behind Collis, near the spruce, beside the stone wall." Specificity commits you to a particular place.

You can rename your spot at any time during the term — click the spot name on your journal page to edit it inline.

Posting a noticing

From your spot's journal page, click + New Noticing. The timestamp is set automatically when you open the compose screen — it records when you are actually at the spot, not when you finished writing.

Writing

Write in the large text area. There is no minimum length — some entries may be a single sentence, others several paragraphs. The text area supports basic markdown: **bold**, *italic*, - lists, > blockquotes, and [links](url). A hint is shown in the compose screen.

Entries can be edited within 48 hours of posting. After that they become permanent — the journal is a record of what you actually noticed at that moment.

Attaching media

TypeHowLimit
PhotoClick photo and browse, or drag a file onto the drop zone. Uploads directly — no leaving the app.3 per entry, max 20 MB each
AudioClick audio and attach a voice memo or field recording. Drag and drop also works.1 per entry, max 60 seconds
VideoUpload to YouTube (unlisted is fine), then click video and paste the link.1 per entry — keep it brief
💡

Click any photo in a journal to enlarge it. Photos are automatically compressed on upload — a 4 MB phone photo becomes roughly 400 KB with no visible quality loss.

Changing your spot

You can abandon your spot and claim a new one at any time during the term. On your journal page, click abandon spot at the bottom.

⚠️ This permanently deletes your spot and all your entries. There is no undo.

After abandoning you'll be taken back to the map to claim a new spot immediately.

Reading other journals

All journals are public. Click browse in the navigation to see every spot this term. Click any pin on the map to jump to that person's journal.

Tips for good noticing

  • Visit often. The value compounds. A spot visited twice a week for nine weeks reveals things a single long visit never would.
  • Notice what has changed. What is different since your last visit? What has appeared? What has gone?
  • Follow your curiosity. If something catches your attention, research it. Your journal can contain research as well as observation.
  • Use all your senses. What do you hear? What does the light do at this hour? Is there a smell?
  • Don't wait for something to happen. The point is to discover that something is always happening.
  • Be specific. "A bird" is less useful than "a pigeon with a gray band on its left leg." Specificity sharpens attention.

The admin dashboard

Sign in and go to /admin. The dashboard is the control centre for the project. You'll use it at the start and end of each term, and occasionally to resolve flagged content.

Dashboard sections

SectionWhat it does
TermsCreate, activate, and archive terms. Only one term can be active at a time.
Registration whitelistPaste your class email list. Only whitelisted emails can register.
StudentsSee all registered students, their spots, and entry counts. Remove accounts if needed.
Flagged contentAppears when students flag an entry. Dismiss the flag or delete the entry.

Term lifecycle

Each term moves through three states:

StateWhat students can do
draftNothing — the term isn't visible to students yet
activeRegister, claim spots, post observations, abandon and reclaim spots
archivedRead their journals — all posting is locked
💡

Archived journals remain publicly readable forever at their original URLs. Students from past terms can still sign in and read their work.

Managing the whitelist

At the start of each term, paste your class list from your course management system into the whitelist field in /admin. One email per line. The app also accepts comma- or semicolon-separated lists.

The whitelist table shows two groups: Not yet registered and Registered. Use this in the first week of term to follow up with students who haven't created their accounts yet.

Emails are reusable — a returning student from a prior term just signs in without re-registering. Removing an email from the whitelist doesn't delete their account; it only prevents new registrations with that address.

Geofence editor

Go to /admin/geofence to adjust the campus boundary. Click points on the map to trace the perimeter, use Undo last to correct mistakes, and click Copy GeoJSON to clipboard when done.

Paste the result into GEOFENCE_POLYGON in your Vercel environment variables, then redeploy. Students trying to claim a spot outside the boundary will see a clear error message.

Storage & limits

ServiceFree tierEstimated usage (30 students, 9 weeks)
Cloudinary10 GB storage, 20 GB/mo bandwidth~1–2 GB with auto-compression
Vercel Postgres256 MB storage, 60 compute hours/monthWell under 10 MB (text and URLs only)
Vercel100 GB bandwidth/moNegligible at this scale
💡

You could run approximately 5–6 terms before approaching Cloudinary's free storage limit. Check usage at cloudinary.com/console mid-term.

Troubleshooting

ProblemFix
Students can't claim spotsCheck that a term is set to active in /admin
"Outside campus boundary" errorGo to /admin/geofence and expand the polygon
Cloudinary uploads failingCheck that your upload preset is Unsigned in Cloudinary → Settings → Upload
Registration blocked unexpectedlyConfirm the student's email is in the whitelist, exactly as typed
Database errors on VercelCheck that the Postgres database is created in Vercel Storage and that POSTGRES_PRISMA_URL appears in your environment variables.
Site not reflecting code changesVercel → Deployments → Redeploy

Project file structure

src/ app/ api/ spots/ # claim and list spots entries/ # post and read journal entries terms/ # create, activate, archive terms users/ # registration and current user media/ # Cloudinary upload signing admin/ # whitelist, flags, user management auth/ # NextAuth route handler admin/ # instructor dashboard + geofence editor map/ # main campus map view spot/[id]/ # individual spot journal (public) new-noticing/ # compose screen browse/ # public spot directory login/ register/ # auth pages components/ # shared UI components lib/ prisma.ts # database client auth.ts # NextAuth config cloudinary.ts # media upload helpers geofence.ts # campus boundary check upload.ts # client-side upload utility middleware.ts # route protection prisma/ schema.prisma # data model

Overview for IT & Developers

The Noticing Project is a Next.js 14 application using the App Router, deployed to Vercel, with a Neon PostgreSQL database and Cloudinary for media storage. This page covers the architecture, environment setup, deployment pipeline, and maintenance tasks.

Tech stack

LayerTechnologyNotes
FrameworkNext.js 14 (App Router)React server components, API routes, middleware
LanguageTypeScriptStrict mode, moduleResolution: node, target es2017
DatabaseNeon (PostgreSQL)Prisma ORM v5.14, two connection strings required (pooled + direct)
AuthNextAuth.js v5 betaCredentials provider, JWT sessions, cookie-based middleware
MediaCloudinaryDirect browser upload via unsigned preset, auto-compression on images
HostingVercelAuto-deploys from GitHub main branch
MapsLeaflet + OpenStreetMapLoaded dynamically to avoid SSR issues
Geofence@turf/boolean-point-in-polygonGeoJSON polygon stored in env var

Environment variables

All variables are set in Vercel's environment variable dashboard. The Neon integration adds the database variables automatically. Others must be added manually.

VariableSourceNotes
POSTGRES_PRISMA_URLNeon (auto)Pooled connection for runtime queries
POSTGRES_URL_NON_POOLINGNeon (auto)Direct connection for migrations
NEXTAUTH_SECRETManualGenerate with openssl rand -base64 32. Mark sensitive.
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAMECloudinary dashboardPublic — safe to expose to browser
CLOUDINARY_API_KEYCloudinary dashboard
CLOUDINARY_API_SECRETCloudinary dashboardMark sensitive
NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESETCloudinary dashboardMust be set to Unsigned in Cloudinary
GEOFENCE_POLYGONAdmin geofence editorGeoJSON coordinates array as JSON string

Local development

Prisma reads .env, not .env.local. The simplest solution is to put all variables in .env.local and create a symlink so Prisma can find them:

.env.local (single file for everything — do not commit)
POSTGRES_PRISMA_URL="postgresql://..." POSTGRES_URL_NON_POOLING="postgresql://..." NEXTAUTH_SECRET="..." NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="..." CLOUDINARY_API_KEY="..." CLOUDINARY_API_SECRET="..." NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET="noticing-project" GEOFENCE_POLYGON='[[...]]'
Create symlink once (makes .env point to .env.local)
ln -s .env.local .env
💡

Both .env and .env.local are already in .gitignore — the symlink won't be committed. This means one file to maintain instead of two.

⚠️ If you ever need to create a temporary .env file for a one-off Prisma command (e.g. pushing schema to production), delete it immediately afterward — it will override the symlink.

Database schema changes

The schema lives in prisma/schema.prisma. After any change, push to the database using the Prisma v5 CLI (not v7+):

# Always use prisma@5.14.0 — v7 broke the schema format npx prisma@5.14.0 db push

⚠️ Adding a required column to a table with existing rows will fail unless you provide a @default() value. Always add defaults to new required fields.

Deployment pipeline

Every push to the main branch on GitHub triggers an automatic Vercel deployment. The build command is prisma generate && next build — Prisma generates the client before Next.js compiles.

💡

To deploy manually from the CLI: vercel deploy --prod

Authentication notes

The app uses NextAuth v5 beta with a credentials provider. A few non-obvious details:

  • The middleware (src/middleware.ts) checks for the session cookie directly rather than using getToken — this avoids an Edge Runtime incompatibility with bcryptjs
  • The session cookie is named __Secure-authjs.session-token in production and authjs.session-token in development
  • NEXTAUTH_URL does not need to be set — NextAuth v5 detects it automatically from the request

Media upload flow

Media never touches the Next.js server. The flow is:

1
Browser requests a signed upload URL

POST to /api/media/sign with { type: "image" | "audio" }. Returns a Cloudinary signature, timestamp, and config.

2
Browser uploads directly to Cloudinary

POST to https://api.cloudinary.com/v1_1/{cloud}/image/upload with the file and signature. Images are auto-compressed to max 1200px wide.

3
Browser sends URL to app server

The Cloudinary URL is included in the entry POST body. Only the URL string is stored in the database.

Project structure

src/ app/ api/ auth/[...nextauth]/ # NextAuth route handler spots/ # GET list, POST create (geofence checked) spots/[id]/ # GET, PATCH rename, DELETE entries/ # GET list, POST create entries/[id]/ # PATCH edit (48hr window), DELETE entries/[id]/flag/ # POST flag for review terms/ # GET list, POST create (admin) terms/[id]/ # PATCH activate/archive, DELETE draft terms/active/ # GET current active term users/register/ # POST — whitelist checked users/me/ # GET current session user media/sign/ # POST — returns Cloudinary upload signature admin/whitelist/ # GET, POST bulk add, DELETE admin/users/[id]/ # DELETE student account admin/flags/[id]/ # PATCH resolve flag admin/ # Instructor dashboard admin/geofence/ # Geofence polygon editor map/ # Campus map with spot claiming spot/[id]/ # Public journal page new-noticing/ # Compose screen browse/ # Public spot directory login/ register/ # Auth pages components/ NavClient # Top nav — uses useSession() Lightbox # Full-screen image viewer MarkdownBody # Lightweight markdown renderer EntryEditor # Inline entry edit (48hr window) SpotNameEditor # Inline spot rename AbandonSpot # Delete spot with confirmation TermManager # Admin term controls WhitelistManager # Admin email whitelist StudentList # Admin student overview FlaggedEntries # Admin content moderation lib/ prisma.ts # Singleton Prisma client auth.ts # NextAuth config + requireAuth/requireAdmin helpers cloudinary.ts # Upload signing, media limits geofence.ts # Campus boundary check (Turf.js) upload.ts # Client-side Cloudinary upload utility middleware.ts # Route protection via session cookie check prisma/ schema.prisma # Data model