Last updated: 2026-07-03 Status: draft: product name, domain, and auth provider open.
Punta = org-grade link shortener + event management + check-in, with campaign analytics that connect clicks to attendance.
Campus and community orgs repeatedly stitch together:
- a URL shortener for posters,
- a form for registration,
- a spreadsheet for attendance,
- and screenshots for "analytics."
Each piece works in isolation but nothing connects the poster QR to who showed up. Punta treats campaign link → event page → registration → check-in as one funnel with org branding and exports.
| Capability | Generic link tool | Generic form | Punta |
|---|---|---|---|
| Short URL + QR | ✓ | : | ✓ |
| Custom landing page | : | partial | ✓ event-native |
| Registration | : | ✓ | ✓ tied to event |
| Per-channel attribution | basic clicks | : | clicks → signups → check-ins |
| Door check-in | : | add-on / manual | ✓ built-in QR ticket |
| Org team roles | : | limited | owner / editor / door / analyst |
| Attendance export | : | manual | ✓ CSV with campaign source |
| Campus venue link | : | : | Room TBA deep link |
Design principle: optimize for org volunteers running 3–10 events per sem, not enterprise marketing suites.
- Org with slug, name, logo upload (optional).
- Event: title, datetime, timezone
Asia/Manila, venue text + optional Room TBA URL, capacity, description (markdown). - Campaign links: multiple short links per event (
/l/{code}) with labels (poster, IG, partner). - Public event page: mobile-first; register button.
- Registration form: name, email, optional custom fields (college, year); waitlist when full.
- Confirmation: on-screen ticket + QR encoding
registrationId. - Check-in scanner (volunteer logged in): scan ticket → mark present.
- Dashboard funnel: per link: clicks, registrations, check-ins.
- CSV export of registrants with source link label.
- Auth: email magic link for organizers; attendees register without account.
- Email confirmation with ticket QR (Resend/Postmark).
- Event duplicate / template from past event.
- Embed mode:
iframeregistration widget for org website. - Webhook:
registration.created,checkin.completed. - Discord notification via uplbtools bot pattern.
- Custom domain
links.yourorg.org(CNAME). - Recurring events series.
- Walk-in registration at door (staff adds row).
- Basic spam protection (Turnstile/hCaptcha on public register).
- Payment / ticket sales (GCash integration is a separate epic).
- Full CRM, email campaigns, seating charts.
- Public link directory / SEO browse (orgs are unlisted unless shared).
- ES org creates "GBM March" event, capacity 200, venue "DL Umali Hall" + Room TBA link.
- Creates links:
gbm-poster,gbm-fb,gbm-partner-cssc. - Poster QR →
gbm-poster→ landing → 180 register, 20 waitlisted. - Door volunteer opens Scanner → 142 check-ins.
- Secretary exports CSV; funnel shows FB link drove most clicks but poster drove most check-ins.
MVP supports redirect links under org namespace for non-event use (announcements, docs). Same analytics shell; registration disabled. Keeps orgs from needing a second tool for pure short links.
flowchart TB
subgraph public [Public]
LP[Landing / event page]
REDIR[Redirect /l/code]
REG[Register API]
end
subgraph org [Organizer app]
DASH[Dashboard]
SCAN[Check-in scanner]
end
subgraph backend [Backend]
API[Astro or Hono API]
DB[(Postgres)]
KV[(Redis: click buffer)]
end
REDIR --> KV
REDIR --> LP
LP --> REG
REG --> DB
DASH --> API
SCAN --> API
API --> DB
KV --> API
| Choice | Recommendation | Rationale |
|---|---|---|
| App framework | Astro 7 + Svelte islands | Matches Room TBA; SSR for public pages |
| DB | Supabase Postgres + Drizzle | Same ops story as sibling repos |
| Redirect edge | Cloudflare Worker or Vercel edge | Low-latency 302 + click log |
| Auth (org) | Supabase Auth magic link | Avoid password support burden |
| File storage | R2 / Supabase storage | Org logos |
| Resend | Transactional tickets |
Click analytics: edge logs (linkId, ts, ua hash) to Redis stream; worker aggregates hourly into Postgres to avoid write amplification on redirect.
type Event = {
id: string;
orgId: string;
slug: string;
title: string;
startsAt: string;
endsAt: string | null;
timezone: "Asia/Manila";
venueName: string | null;
venueUrl: string | null; // Room TBA, Google Maps, etc.
capacity: number | null;
waitlistEnabled: boolean;
descriptionMd: string;
status: "draft" | "published" | "archived";
};
type CampaignLink = {
id: string;
eventId: string | null; // null = pure redirect
code: string; // short code globally unique
label: string; // "poster", "instagram"
destinationUrl: string; // redirect target OR event page if eventId set
clickCount: number;
};
type Registration = {
id: string;
eventId: string;
campaignLinkId: string | null;
email: string;
name: string;
fields: Record<string, string>;
status: "confirmed" | "waitlist" | "cancelled";
checkInAt: string | null;
ticketToken: string; // signed, in QR
};
type ClickEvent = {
linkId: string;
occurredAt: string;
referrer: string | null;
};Ticket QR: JWT or HMAC { registrationId, eventId, exp }: scanner validates offline-capable with cached event public key (v0.2).
| URL | Purpose |
|---|---|
punta.example/l/{code} |
Redirect + click log → event or external URL |
punta.example/e/{code} |
Alias to event landing (optional) |
punta.example/o/{orgSlug}/{eventSlug} |
Canonical event page |
punta.example/o/{orgSlug} |
Org public list (published events) |
punta.example/app/* |
Organizer dashboard (auth required) |
punta.example/scan/{eventId} |
Check-in mode (door role) |
Ship presets orgs can enable:
- College / unit
- Year level
- Org affiliation (member / guest)
- Dietary restrictions
- Accessibility needs
Stored as JSON schema per event; render dynamically on registration form.
-
link-core: generate code, store mapping,302redirect - Click counter (Postgres increment or Redis buffer)
- Deploy edge redirect on staging subdomain
Exit: Create link → 100 redirects → count accurate.
- Org + magic-link auth
- Event CRUD, publish landing page
- Campaign links tied to event
- Registration + waitlist + capacity
- Ticket QR display
- Check-in scanner marks
checkInAt - Dashboard funnel + CSV export
Exit: End-to-end test event with 2 campaign links and 10 test registrations.
- Email ticket
- Webhooks + Discord notify
- Room TBA venue picker helper (search room → paste URL)
- Registration embed widget
- Custom domain docs
- Rate limits + abuse monitoring
- Pro tier design (only if free tier costs bite)
| Layer | Focus |
|---|---|
link-core |
Code uniqueness, expired links, circular redirect guard |
| Registration | Capacity race (two last slots), waitlist promotion |
| Check-in | Double scan idempotent, invalid token rejected |
| Integration | Full funnel Playwright: click link → register → scan |
| Security | Ticket forgery, org IDOR, CSV export auth |
| Concern | Target |
|---|---|
| Redirect p95 | < 50ms at edge |
| Registration POST | < 500ms |
| Uptime | 99.5% (free tier honest SLA) |
| Data | Org can export + delete all registrants for an event |
| PII | Email encrypted at rest optional; never sell data |
- Domain:
punta.uplbtools.mevs standalone brand? - Attendee accounts: allow "my tickets" login or ticket QR only?
- Multi-org users: one email in many orgs: standard Supabase model.
- Moderation: report abusive public pages: manual queue at launch?
- Offline check-in: queue scans when gym Wi‑Fi dies: v0.2 or v0.3?
- Phase 0 issue in this repo.
- Validate name Punta with org officers (confusing vs "punta sa…"?): alias repo if renamed.
- Wireframe: event landing + scanner on 320px.
- Discord skill doc stub for webhook payload shape.
Goal: orgs stop exporting Forms to Sheets to prove attendance. One funnel, one export.