Skip to content

Repository files navigation

⚡ TechPulse

Every tech opportunity across Ghana & West Africa. Sorted by deadline, straight to your inbox.

TechPulse is a deadline-first platform for students and early-career builders in Ghana & West Africa. It gathers the opportunities they keep missing — hackathons, internships, scholarships, bootcamps, workshops, conferences, meetups and webinars — and keeps subscribers updated by email:

  • 📡 Auto-import sources — register iCal or JSON event feeds and the platform syncs them daily, dedupes, and keeps changed events up to date on its own.
  • 📣 Instant announcements — when a new program is published, every subscriber whose interests match its category gets an email (imports are batched into one email per subscriber).
  • 🗓️ Weekly digest — a personalised summary of programs starting in the next two weeks.
  • Deadline reminders — when a program's registration deadline is within a few days, matching subscribers get a "closing soon" email (once per program). Browse deadline-first, and see prize pools at a glance.
  • 🎛️ Self-service preferences — every email carries a personal link to change interests or unsubscribe.

Built with Node.js, Express, and SQLite (via Node's built-in node:sqlite) — no external database or build step required.

Quick start

Requires Node.js 22.5+.

npm install
npm run seed     # load sample events (optional)
npm start        # http://localhost:3000

Then open:

Page URL
Public site (browse + subscribe) http://localhost:3000/
Admin console (publish programs) http://localhost:3000/admin.html
Email preferences (linked from emails) http://localhost:3000/preferences.html?token=…

The admin console asks for a token — in development it defaults to dev-admin-token.

How email works

Mode When What happens
Outbox (default) SMTP_HOST not set Every email is written to ./outbox/*.html so you can open and inspect it. Perfect for development.
SMTP SMTP_HOST set Email is delivered via nodemailer using your SMTP credentials.

Emails sent:

  1. Welcome — on subscribe / resubscribe.
  2. New program announcement — to matching subscribers when an admin publishes an event.
  3. Weekly digest — personalised upcoming-events roundup. Sent automatically by the in-server scheduler (Mondays 09:00 by default), or on demand via the admin console / npm run digest (handy for cron: 0 9 * * 1 cd /path/to/app && npm run digest).
  4. Deadline reminder — when a program's registration_deadline falls within DEADLINE_REMINDER_DAYS (default 3), matching subscribers get one "closing soon" email per program. Sent by the in-server scheduler (hourly check) or on demand from the admin console.

Where does event data come from?

Four ways in, from hands-on to fully automatic:

  1. Capture inbox — forward an opportunity from WhatsApp, LinkedIn or anywhere else and it lands in a review queue as a pre-filled draft; one click publishes it and emails subscribers. See Capture inbox below.

  2. Admin console — publish programs by hand at /admin.html. Good for curated one-offs; subscribers are emailed instantly.

  3. Auto-import sources (recommended) — register feeds in the admin console ("Auto-import sources" panel) and the platform keeps itself fresh:

    • iCal (.ics) feeds — almost every tech community publishes one: Google Calendar (public calendar → "Public address in iCal format"), Luma calendars (ical subscribe link), Meetup group calendar exports, university event calendars, GDG chapter calendars.
    • JSON feeds — any endpoint you or a partner controls, shaped like:
      { "events": [ { "id": "unique-id", "title": "CodeStorm 2026", "category": "hackathon",
          "starts_at": "2026-08-01T09:00:00Z", "ends_at": "2026-08-03T18:00:00Z",
          "location": "Kumasi, Ghana", "url": "https://…", "organizer": "",
          "description": "", "tags": ["fintech"] } ] }

    The sync engine dedupes by feed UID, updates events that changed at the source (reschedules, renames), skips events that already ended, and infers a category from the title ("…Hackathon Night" → hackathon, "Study Jam" → workshop) with the source's fallback category as a safety net.

  4. The API — anything that can make an HTTP request can push events: a scraper you run elsewhere, a GitHub Action, Zapier/Make, a partner's backend. POST /api/events with the admin token (see example below).

How "regularly" works: the running server checks every 30 minutes and syncs all sources once per SYNC_INTERVAL_HOURS (default: daily). Not running the server 24/7? Put npm run sync on cron instead: 0 7 * * * cd /path/to/app && npm run sync. After each sync, subscribers get one batched email covering the new programs that match their interests — a feed with 30 new events never means 30 emails.

For sources without feeds (Devpost, Eventbrite, MLH and friends): Eventbrite has an official API (needs an API key), Luma exposes iCal, Meetup's API needs OAuth. Wire any of them to POST /api/events, or transform their API responses into the JSON feed format above and register that endpoint as a source. Check each site's terms before scraping pages that don't offer an API or feed.

Deploy it live (Render, free)

This repo ships a Render blueprint, so a deployment is a few clicks:

  1. Go to dashboard.render.com → sign in with GitHub.
  2. New +Blueprint → pick the feedback-form repo → choose the branch that has this code → Apply.
  3. Wait ~2 minutes for the first build. Render gives you a URL like https://techpulse.onrender.com.

The first boot fills the site automatically — sample opportunities plus the live osman-scraper feed — so the page is never empty.

To sign in to the admin console: in Render open your service → Environment → copy the generated ADMIN_TOKEN, then paste it at https://<your-url>/admin.html.

Good to know about the free plan:

  • The service sleeps after ~15 minutes idle; the next visit takes ~50s to wake.
  • There is no persistent disk, so the database resets on restart (samples and the feed reload automatically, but subscribers are lost). To keep data permanently, upgrade the plan, then uncomment the disk: block in render.yaml and set DB_PATH=/var/data/techpulse.db.
  • Email is written to files inside the container until you add SMTP settings. To send real email, add SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS and MAIL_FROM in the Render dashboard (a free Resend or Brevo account, or a Gmail app password, works).

Capture inbox (forward an opportunity)

Most opportunities in Ghana are announced in WhatsApp groups, on LinkedIn, or as flyers — places no scraper can reach. The capture inbox turns that into a one-tap flow:

forward a message  →  draft extracted  →  review queue  →  publish  →  subscribers emailed

Three ways to capture:

  1. Paste — the "Capture inbox" panel in the admin console takes any pasted message.
  2. Telegram bot — create a bot with @BotFather (free, 2 minutes), set TELEGRAM_WEBHOOK_SECRET, then point the bot at your server:
    curl "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook?url=https://your-host/api/hooks/telegram/$TELEGRAM_WEBHOOK_SECRET"
    Now forwarding any message to the bot queues it for review.
  3. WhatsApp — with a WhatsApp Business Cloud API number (inbound messages are free), set WHATSAPP_VERIFY_TOKEN and register https://your-host/api/hooks/whatsapp as the webhook. Anyone can then forward events to that number — print it on a flyer and your community becomes the collection network.

Extraction: with ANTHROPIC_API_KEY set, Claude reads the message (and flyer captions) and fills in title, category, dates, deadline, location and link. Without a key it still works — a lightweight parser pulls out the title, link and category, and the reviewer fills the rest. Nothing publishes without review.

Configuration

Copy .env.example to .env (loaded automatically via Node's --env-file-if-exists):

Variable Default Purpose
PORT 3000 HTTP port
BASE_URL http://localhost:3000 Base for links inside emails
ADMIN_TOKEN dev-admin-token Auth for admin API/console — change in production
DB_PATH ./data/techpulse.db SQLite database location
SMTP_HOST/PORT/USER/PASS SMTP delivery (unset ⇒ outbox mode)
MAIL_FROM TechPulse <updates@techpulse.local> From header
DIGEST_AUTO true Enable in-server weekly digest scheduler
DIGEST_DAY / DIGEST_HOUR 1 / 9 When the weekly digest goes out (server time)
SYNC_AUTO true Enable in-server source auto-sync
SYNC_INTERVAL_HOURS 24 How often sources are synced
DEADLINE_REMINDER_AUTO true Enable in-server deadline-reminder scheduler
DEADLINE_REMINDER_DAYS 3 Remind when a registration deadline is within N days

API

Public:

Method Route Description
GET /api/events?category=&q=&upcoming=true&closing=true&sort=deadline List events (filter by category, search, upcoming; closing=true = registration still open; sort=deadline = closing-soonest first)
GET /api/events/:id Single event
GET /api/categories Available categories
POST /api/subscribe { email, name?, interests? } — subscribe or update (empty interests = all)
GET /api/preferences?token= Read a subscription (token from email links)
POST /api/preferences { token, interests?, name? } — update a subscription
POST /api/unsubscribe { token } — deactivate a subscription
GET /api/health Health + mail mode

Admin (send header x-admin-token: <ADMIN_TOKEN>):

Method Route Description
POST /api/events Publish a program → emails matching subscribers, returns notified count
PUT /api/events/:id Update a program
DELETE /api/events/:id Remove a program
GET /api/subscribers Subscriber overview
POST /api/digest/send Send the digest immediately
POST /api/reminders/send Send deadline reminders now ({ days? } to widen the window)
GET /api/captures?status=pending|all Review queue
POST /api/captures Queue a pasted message ({ text, image?, from? })
POST /api/captures/:id/approve Publish a capture (body fields override the draft) → emails subscribers
POST /api/captures/:id/reject Discard a capture

Webhooks (no admin token — secured by their own secret): POST /api/hooks/telegram/:secret, GET|POST /api/hooks/whatsapp. | GET | /api/sources | List auto-import sources with last sync status | | POST | /api/sources | Register a feed: { name, type: "ical"\|"json", url, default_category } | | POST | /api/sources/sync | Sync all sources now (returns per-source results + subscribers notified) | | POST | /api/sources/:id/sync | Sync one source now | | DELETE | /api/sources/:id | Remove a source (imported events stay) |

Example — publish a program:

curl -X POST http://localhost:3000/api/events \
  -H 'Content-Type: application/json' \
  -H 'x-admin-token: dev-admin-token' \
  -d '{
    "title": "CodeStorm Hackathon 2026",
    "category": "hackathon",
    "starts_at": "2026-08-01T09:00:00Z",
    "ends_at": "2026-08-03T18:00:00Z",
    "location": "Kumasi, Ghana",
    "url": "https://example.com/codestorm",
    "tags": ["fintech", "students"],
    "description": "48 hours of building."
  }'

Tests

npm test

Covers the subscribe flow (welcome email, dedupe, validation), event CRUD + auth, interest-based announcement targeting, unsubscribe behaviour, digest sending, and filtering.

Project structure

server.js              entry point + digest & source-sync schedulers
src/
  app.js               express app factory
  auth.js              admin-token middleware
  db.js                SQLite schema & helpers (node:sqlite)
  mailer.js            SMTP or outbox-file mail transport
  notify.js            welcome / announcement / batch / digest emails
  sources.js           feed parsers (iCal, JSON) + sync engine
  validate.js          input validation helpers
  routes/
    events.js          event listing & admin CRUD
    sources.js         auto-import source management & sync
    subscribers.js     subscribe, preferences, unsubscribe, digest
public/                static frontend (home, admin console, preferences)
scripts/
  seed.js              sample data (fictional events)
  send-digest.js       cron-friendly digest sender
  sync.js              cron-friendly source sync
test/                  end-to-end API + sync tests

Note: the seed data is fictional sample content (example.com links) for demo purposes.

Ideas for later

  • Public event submission form with moderation queue
  • Double opt-in confirmation emails
  • iCal feed / "add to calendar" buttons
  • API-key connectors for sources without feeds (Eventbrite, Devpost, MLH)

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages