A daily photo journal for Hack Club intern cohorts (could potentially be repurposed for other cohorts). Each day has a prompt for at least one photo; members upload up to 5 photos per day and build streaks. Admins can set prompts and review submissions.
Built with SvelteKit, PostgreSQL (Drizzle ORM), and Hack Club OAuth.
2026-07-02.13-26-06.mp4
For members
- Daily photo prompts, up to 5 uploads per day
- Streak tracking (3+ photos/day and the prompt met keeps your streak alive)
- Like other people's photos to promote the best ones to the top
- Streaks leaderboard
- Per-user notification preferences:
- Daily reminder at a configurable hour
- Web push notifications (when installed as a PWA)
- Slack DM reminders
- Cohort gating: uploads + reminders are paused outside the configured
COHORT_START/COHORT_ENDwindow
1. Install dependencies
npm install2. Configure environment variables
Copy .env.example to .env and fill in the values:
| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
HACKCLUB_AUTH_CLIENT_ID |
OAuth app client ID |
HACKCLUB_AUTH_CLIENT_SECRET |
OAuth app client secret |
HACKCLUB_AUTH_REDIRECT_URI |
OAuth redirect URI |
WHITELIST_IDS |
Comma-separated Slack user IDs with access |
ADMIN_IDS |
Comma-separated Slack user IDs with admin access |
COHORT_START / COHORT_END |
Date range for the cohort (YYYY-MM-DD) |
HACKCLUB_CDN_TOKEN |
Token for Hack Club CDN image hosting |
SLACK_BOT_TOKEN |
For Slack notifications (optional) |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_SUBJECT |
For web push notifications (optional) |
CRON_SECRET |
Secret for cron endpoints |
Generate VAPID keys with npx web-push generate-vapid-keys.
3. Start the database
npm run db:start # starts a local Postgres container via Docker Compose
npm run db:migrate # applies migrations4. Run the dev server
npm run devnpm run build # production build
npm run preview # preview production build
npm run check # type checking
npm run db:studio # Drizzle Studio (visual DB browser)
npm run db:generate # generate migrations from schema changesShutter is a regular SvelteKit app, so it runs on anything that can host one. You'll need:
- Postgres
- A Hack Club Auth app
- Hack Club CDN app — for image hosting (understand that this means your photos will be somewhat public?)
- A way to run a cron job —
hooks.server.tsschedules an in-process hourly tick viacroner, which hits/api/cron/reminders. If your host kills idle processes (most serverless platforms), point a real cron service (Vercel Cron, GitHub Actions on a schedule, etc.) at/api/cron/reminderswith theCRON_SECRETbearer token instead.
Optionally:
- Slack workspace + bot token — only needed if you want DM reminders, the welcome DM on invite, or the admin "broadcast to non-push users" feature
To deploy: set the env vars from the table above, run node migrate.js once to apply migrations, then node build/index.js (or whatever your platform's start command is).
The "cohort" concept is generic - anyone with a defined start/end window can use this - think a Hack Club event, or a group trip, or even a class going through their school year together.
Configuring your cohort
COHORT_START/COHORT_ENDcontrols when images can be submitted.- The 3-photos-per-day streak threshold lives in
src/lib/server/streak.ts. - The 5-upload daily cap is in the upload route (
src/routes/(app)/upload/+server.ts).
The app mentions "Hack Club" and "interns" by default - find instances and replace them as as needed.
Slack bot
The bot needs 5 scopes:
- chat:write - Send DM notifications and forward images to Slack if wanted
- users:read - Fetch user data for profile pictures and more
- chat:write.public - Forward images to public Slack channels the bot isn't in
- channels:read - Get channel info to confirm the bot can access it / post in it
- groups:read - Post in private channels the bot is invited to
Set up a Shutter bot (maybe themed around your specific cohort?) with these scopes, and you're golden!
PRs welcome!
This project used Claude Code for the implementation of various features, writing a good chunk of the code. Most technical workings were designed by me (including the login/auth system, the onboarding flow, integration with CDN and Slack, and more), but not manually implemented with hand-written code.
If you have any further questions about how to host your own instance of Shutter for a group or cohort, feel free to reach out to me on Slack @maxstellar or at my email!