SignPayGo

Developer Project Brief


Platform: ASP.NET MVC (latest) · PostgreSQL · Razor Views

Start URL: https://secure.signpaygo.com/signpaygo-dashboard.html

Status: HTML/CSS/JS prototypes complete — ready for back-end implementation

Document: Confidential — internal use only


SignPayGo is a SaaS platform that lets youth sports coaches, teachers, and event organizers collect permission slips, liability waivers, and payments from parents — all in one step. Parents sign and pay on any device without creating an account.

1. Pages & Routing

All pages are currently static HTML prototypes. Browse every page and interaction at the URL above before writing any code. Implement each as an MVC controller action + Razor view.

HTML FileRoute / ControllerNotes
signpaygo-dashboard.htmlDashboardController / IndexMain landing after login. Shows event stats, recent activity, quick-create CTA.
signpaygo-create-event.htmlEventsController / CreateMulti-section form: details, category, waiver editor, reminders, pricing. POST to EventsController/Store.
signpaygo-participants.htmlEventsController / ParticipantsParticipant list for a specific event. Linked from create-event on publish.
signpaygo-templates.htmlTemplatesController / IndexBrowse My Templates + Starter Templates grid.
signpaygo-create-template.htmlTemplatesController / CreateFull waiver editor for building a reusable template. POST to TemplatesController/Store.
signpaygo-payouts.htmlPayoutsController / IndexStripe payout history and balance. Read from Stripe Connect API.
signpaygo-staff.htmlTeamController / IndexTeam member management. Roles: Owner, Admin, Staff, Viewer.
signpaygo-settings.htmlSettingsController / IndexTabbed: Profile, Organization, Integrations, Billing, Notifications.

1.1 Settings — Integrations Tab

The Integrations tab contains an API & Webhooks section. Hide this entire section using CSS in v1 — it will be surfaced in v2.

#api-webhooks-section { display: none; }

2. PostgreSQL Database Schema

Use snake_case table and column names throughout. Never use full_name — always split into first_name and last_name as separate columns. All tables include the following four audit columns (omitted from each table below for brevity):

created_by UUID FK → users(id)  |  created_on TIMESTAMPTZ NOT NULL DEFAULT now()
modified_by UUID FK → users(id)  |  modified_on TIMESTAMPTZ NOT NULL DEFAULT now()

Use UUID primary keys everywhere (gen_random_uuid()).

users

ColumnTypeNotes
idUUID PKgen_random_uuid()
emailVARCHAR(255)UNIQUE NOT NULL — used as login identifier
first_nameVARCHAR(100)NOT NULL
last_nameVARCHAR(100)NOT NULL
avatar_urlTEXTOptional profile image URL
google_subVARCHAR(255)Google Sign-In subject ID — UNIQUE, nullable
password_hashTEXTbcrypt hash — NULL when using Google SSO only
is_activeBOOLEANDEFAULT true — set false on account closure
last_login_atTIMESTAMPTZUpdated on every successful auth

organizations

One user can belong to many orgs; one org can have many users (via org_members).

ColumnTypeNotes
idUUID PK
nameVARCHAR(255)Organization / club / school name
logo_urlTEXTUploaded logo for branding on waivers
stripe_account_idVARCHAR(255)Stripe Connect Express account ID — for receiving payments from parents
stripe_customer_idVARCHAR(255)Stripe Customer ID — used to charge the org for their SignPayGo subscription
stripe_onboarding_doneBOOLEANDEFAULT false — flipped after Stripe onboarding completes
default_categoryVARCHAR(50)Default event category for this org
planVARCHAR(50)DEFAULT 'free' — values: free, pro, team, enterprise
plan_billing_price_centsINTEGERSubscription amount in cents (e.g. 94800 = $948/yr)
plan_last_billed_onDATEDate of the most recent successful subscription charge
plan_next_bill_onDATEDate the next subscription charge will be attempted
cc_fee_percentNUMERIC(5,2)DEFAULT 3.90 — credit card processing rate applied to parent payments. Editable per org for custom pricing agreements.

org_members

ColumnTypeNotes
idUUID PK
org_idUUID FKREFERENCES organizations(id) ON DELETE CASCADE
user_idUUID FKREFERENCES users(id) ON DELETE CASCADE
roleVARCHAR(50)Values: owner, admin, staff, viewer
invited_atTIMESTAMPTZ
accepted_atTIMESTAMPTZNULL until invite is accepted

events

ColumnTypeNotes
idUUID PK
org_idUUID FKREFERENCES organizations(id)
created_byUUID FKREFERENCES users(id)
titleVARCHAR(255)NOT NULL
categoryVARCHAR(50)sports, field-trip, camp, arts, religious, general
descriptionTEXTOptional rich text
locationTEXT
starts_atTIMESTAMPTZ
ends_atTIMESTAMPTZ
slugVARCHAR(120)UNIQUE — URL-safe slug for the public sign-up page
statusVARCHAR(30)draft, active, completed, cancelled
participant_modeVARCHAR(20)list, self, both
waiver_contentTEXTHTML from the WYSIWYG editor including field-token spans
price_centsINTEGERNULL = free event. Stored in cents.
tax_percentNUMERIC(5,2)e.g. 8.25
processing_fee_modeVARCHAR(20)absorb, pass_on
deposit_enabledBOOLEANDEFAULT false
deposit_centsINTEGERNULL when deposit_enabled = false
balance_due_atDATEDate remainder is due
template_idUUID FKNULL if not started from a template

event_reminders

ColumnTypeNotes
idUUID PK
event_idUUID FKREFERENCES events(id) ON DELETE CASCADE
send_atDATEAbsolute date the reminder fires
is_enabledBOOLEANDEFAULT true
sent_atTIMESTAMPTZNULL until actually sent

participants

ColumnTypeNotes
idUUID PK
event_idUUID FKREFERENCES events(id) ON DELETE CASCADE
child_first_nameVARCHAR(100)NOT NULL
child_last_nameVARCHAR(100)NOT NULL
parent_first_nameVARCHAR(100)NOT NULL
parent_last_nameVARCHAR(100)NOT NULL
parent_emailVARCHAR(255)
parent_phoneVARCHAR(50)
statusVARCHAR(30)pending, signed, paid, signed_paid, cancelled
signed_atTIMESTAMPTZNULL until waiver is submitted
paid_atTIMESTAMPTZNULL until payment confirmed
amount_paid_centsINTEGERTotal amount charged to parent (base price + processing fee)
fee_centsINTEGERCredit card processing fee portion of the charge — for reporting. Calculated as round(price_cents × cc_fee_percent / 100) at checkout.
stripe_payment_idVARCHAR(255)Stripe PaymentIntent ID
fingerprint_idVARCHAR(255)FingerprintJS visitor ID for fraud prevention
ip_addressINETCaptured at time of signing
device_infoJSONBUser agent, screen size, etc.
field_responsesJSONBKey/value map of all waiver field answers
signature_dataTEXTBase64 PNG of drawn signature
initials_dataTEXTBase64 PNG of drawn initials
signed_pdf_pathTEXTCloud storage path to the locked signed PDF — e.g. waivers/{org_id}/{event_id}/{participant_id}.pdf. All three path segments are UUID v4 — never a sequential integer. Never changes after creation.
signed_pdf_urlTEXTPre-signed or CDN download URL with a short expiry (e.g. 15 minutes). Pre-signed URLs are the primary access control layer — even knowing the storage path does not grant access because the bucket is private. Regenerate from signed_pdf_path on demand when the URL expires.

waiver_templates

ColumnTypeNotes
idUUID PK
org_idUUID FKREFERENCES organizations(id) ON DELETE CASCADE
created_byUUID FKREFERENCES users(id)
nameVARCHAR(255)NOT NULL
descriptionTEXT
categoryVARCHAR(50)Same category enum as events
visibilityVARCHAR(20)private, team
contentTEXTHTML body including field-token spans
use_countINTEGERDEFAULT 0 — incremented each time applied to an event

email_log

ColumnTypeNotes
idUUID PK
org_idUUID FK
event_idUUID FKNULL for non-event emails
participant_idUUID FKNULL for broadcast emails
to_emailVARCHAR(255)
subjectVARCHAR(500)
template_keyVARCHAR(100)invitation, reminder, receipt, signup_confirm, waiver_complete
provider_msg_idVARCHAR(255)SendGrid / SES message ID for tracking correlation
statusVARCHAR(30)queued, sent, delivered, opened, clicked, bounced, spam
opened_atTIMESTAMPTZ
clicked_atTIMESTAMPTZ
bounced_atTIMESTAMPTZ
sent_atTIMESTAMPTZ

payouts

ColumnTypeNotes
idUUID PK
org_idUUID FK
stripe_payout_idVARCHAR(255)Stripe Payout object ID
amount_centsINTEGER
currencyVARCHAR(10)DEFAULT 'usd'
arrival_dateDATE
statusVARCHAR(30)pending, paid, failed, cancelled
descriptionTEXT

3. Third-Party Integrations

3.1 Stripe Connect — Payments & Routing

Each organization connects their own Stripe account via Stripe Connect Express. SignPayGo acts as the platform account; orgs are connected accounts. Funds flow: parent → Stripe → org's connected account (minus platform fee).

⚠️ stripe_onboarding_done must be verified server-side via the Stripe API — do not rely solely on the redirect callback, which can be tampered with.

3.1.1 Accepted Payment Methods

The parent-facing signing page must support all three of the following payment methods. Stripe handles the rendering and authentication for each — no third-party SDKs required beyond the Stripe JS library.

MethodImplementationNotes
Credit & Debit Card Stripe Elements — CardElement or Payment Element Visa, Mastercard, Amex, Discover. Use Stripe Payment Element (not the legacy Card Element) — it handles all three methods in a single UI component with automatic method detection.
Apple Pay Built into Stripe Payment Element Automatically shown on Safari / iOS when the device has Apple Pay configured. No additional code — Stripe handles domain verification and the payment sheet. Requires the domain to be registered in the Stripe dashboard.
Google Pay Built into Stripe Payment Element Automatically shown on Chrome / Android when the browser has a saved card. No additional code — handled entirely by Stripe Payment Element. Works on desktop Chrome and Android Chrome.
Use the Stripe Payment Element (not the legacy Card Element or separate Apple Pay / Google Pay buttons). It detects what the parent's device supports and shows the appropriate options automatically — one integration, three methods.

3.1.2 Credit Card Processing Fee

SignPayGo passes the credit card processing fee to the parent at checkout. The fee rate is configurable per organization to accommodate custom pricing agreements.

3.1.3 Refunds

Full refunds are initiated by the organizer from the participant view modal. The UI confirmation is already implemented in the frontend. The server-side implementation must:

⚠️ Never delete the signed PDF from S3 on refund. The WORM object lock prevents it anyway, but the application layer must not attempt to delete it. The PDF is a legal record of what was signed — it must be retained regardless of the refund status.

3.2 Google Sign-In

3.3 FingerprintJS — Device Identity & Fraud Prevention

3.4 Email — SendGrid or AWS SES

Use SendGrid as the primary provider (preferred) or AWS SES as fallback. Route all outbound email through a single IEmailService abstraction so the provider can be swapped without touching calling code. Track opens, clicks, and bounces via SendGrid Event Webhooks or SES SNS notifications. Write every outbound email to the email_log table including provider_msg_id. On bounce, mark the participant's email as invalid and surface a warning in the participants view.

⚠️ Never hardcode SMTP credentials. Use environment variables / Azure Key Vault / AWS Secrets Manager.

3.4.1 Email Templates

There are three parent-facing transactional templates plus supporting organizer and system templates. The three parent templates are HTML files checked into the repository and rendered server-side with template variable substitution.

Template KeyFileSent ToTrigger
invitationemail-invitation.htmlParentOrganizer publishes event and adds participants — see 3.4.2
reminder_sign_payemail-reminder-sign-pay.htmlParentScheduled reminder fires and participant has NOT signed AND NOT paid — see 3.4.3
reminder_payment_onlyemail-reminder-payment-only.htmlParentScheduled reminder fires and participant HAS signed but has NOT paid — see 3.4.3
receipt(TBD)ParentStripe payment confirmed — attach signed waiver PDF
signup_confirm(TBD)ParentParent self-registers via the public sign-up link
waiver_complete(TBD)OrganizerAll participants in an event have reached signed_paid status

3.4.2 Invitation Email — When It Sends

The invitation email (email-invitation.html) is the very first email a parent receives. It is sent once per participant when the organizer publishes and sends the event. It is never sent again — subsequent emails are always reminders, not invitations.

3.4.3 Reminder Emails — When They Send

Reminders are configured by the organizer when creating or editing an event. Each reminder is stored as a row in event_reminders with a send_at date and an is_enabled flag. A background job (Hangfire) runs daily and dispatches any reminders whose send_at date matches today and whose sent_at is still NULL.

The reminder template is chosen dynamically per participant based on their current status at the time the job runs:

Participant statusTemplate sentReasoning
pending — not signed, not paidreminder_sign_payParticipant needs to do both steps. Email covers signing and payment together.
signed — signed but not paidreminder_payment_onlyWaiver is on file. Email acknowledges this and focuses only on the outstanding payment.
signed_paid — fully completeNo email sentNothing to do. Skip this participant silently.
cancelledNo email sentDo not contact cancelled participants.

After sending each batch for a reminder row, set event_reminders.sent_at = NOW() to prevent re-sending. Log each individual email to email_log with the correct template_key.

3.4.4 Reminder Schedule — Set on the Create Event Page

The organizer configures reminder dates on the Create Event / Edit Event page in the Reminders section of the form. Each date the organizer sets creates one row in event_reminders. Reminders can be individually toggled on/off via is_enabled without deleting the row.

3.4.5 Template Variable Reference

All three parent-facing templates share the same variable naming convention. Populate them server-side before sending. The sign_pay_url must be a unique, participant-scoped URL — never a generic event link.

VariableSource
{{parent_first_name}}participants.parent_first_name
{{child_first_name}}participants.child_first_name
{{child_last_name}}participants.child_last_name
{{event_name}}events.title
{{org_name}}organizations.name
{{event_dates}}Formatted from events.start_date / events.end_date
{{event_location}}events.location
{{deadline_date}}Formatted from events.deadline
{{amount_due}}Formatted from events.price_cents (e.g. "$285.00")
{{sign_pay_url}}Generated URL: https://signpaygo.com/r/{event_slug}/?p={participant_id}
{{pay_url}}Same as above but deep-links to the payment step (reminder_payment_only only)
{{organizer_name}}users.first_name + last_name of the event owner
{{organizer_email}}users.email of the event owner
{{unsubscribe_url}}Generated unsubscribe link scoped to participant_id

3.5 Zapier V2 — Coming Soon

Zapier integration is deferred to v2. No work required in v1. When implemented, it will fire outbound webhook triggers on key participant and event lifecycle events (signed, paid, completed). The Settings → Integrations tab shows Zapier with a "Coming Soon" badge and a disabled "Notify Me" button.

3.6 Make.com V2 — Deferred

Make.com integration is planned for v2. No work required in v1.

4. Template Data — Past vs. Library

4.1 Past Templates — Move to Database

Past templates currently load from PastTemplates.js. In the MVC implementation they must come from the waiver_templates table, scoped to the logged-in user's organization.

4.2 SignPayGo Library — Keep as JSON

The shared SignPayGo Library templates can remain as a static JSON file served from wwwroot for v1. No database table required.

5. PDF Generation & Cloud Storage

5.1 Signed Waiver PDF

When a parent submits a signed waiver, the server immediately generates a locked PDF that acts as the permanent legal record. The events.waiver_content column remains the live editable version — the PDF is the frozen-at-signing snapshot. The site owner changing their waiver template after the fact has no effect on previously signed documents.

5.2 What the PDF Must Contain

5.3 Generation & Storage Flow

  1. Parent submits the signing form (POST to /sign/{slug}).
  2. Server validates all required fields and the FingerprintJS visitorId.
  3. Server renders the PDF server-side using a library such as PuppeteerSharp (headless Chrome) or iTextSharp / QuestPDF.
  4. PDF is written to cloud storage using the path convention: waivers/{org_id}/{event_id}/{participant_id}.pdf — all three IDs are UUID v4 (never sequential integers). A UUID has ~122 bits of entropy, making path enumeration attacks computationally infeasible. Never substitute a numeric or auto-increment ID in this path.
  5. The storage path and a pre-signed download URL are saved to participants.signed_pdf_path and participants.signed_pdf_url.
  6. An email is sent to the parent with the PDF attached (using the receipt email template).
  7. The organizer's participants view shows a "Download PDF" link using the stored URL.

5.4 Cloud Storage — AWS S3

Use AWS S3 (Simple Storage Service) for signed waiver PDF storage. S3 is object storage — files are written once, retrieved by path, and served via pre-signed URL. It is the correct service for this pattern. Do not use EFS (file system), EBS (block storage attached to a single EC2 instance), or RDS (database) for PDF storage.

5.4.1 S3 Bucket Configuration

Create a dedicated private bucket for signed waivers (e.g. signpaygo-waivers-prod). Apply all of the following settings at bucket creation — some cannot be changed after PDFs have been written.

SettingValueWhy
Block Public AccessAll four options enabledNo file in this bucket is ever served directly. All access goes through pre-signed URLs. This must be on even if object policies look correct — it is a hard override.
VersioningEnabledRequired by Object Lock. Also protects against accidental overwrites.
Object LockEnabled — Compliance modeWORM (Write Once Read Many). In Compliance mode, not even the AWS root account can delete or overwrite a locked object before the retention period expires. Governance mode allows admins to override — do not use it for signed legal documents.
Object Lock retention period7 years (2555 days)Matches the lifecycle rule. PDFs are immutable for the full retention window.
Server-side encryptionSSE-S3 (AES-256, AWS-managed keys)Encrypts data at rest. SSE-S3 is simpler to operate than SSE-KMS and sufficient for this use case unless compliance requires customer-managed keys.
ACLsDisabled (Bucket Owner Enforced)ACLs are a legacy access model. Disabling them ensures all access is controlled exclusively through bucket policies and IAM — no object-level ACL confusion.
Transfer accelerationOptionalEnable if parents are geographically distributed and upload latency is a concern. Not required on launch.

5.4.2 IAM Access

5.4.3 Pre-Signed URLs

5.4.4 Lifecycle Rules

⚠️ PDFs must never be regenerated from current waiver content — only from the snapshot embedded at signing time. If a URL expires, regenerate the pre-signed URL pointing to the original file, not the file itself.

6. Authentication & Authorization

⚠️ The parent signing page handles PII. Ensure it is served over HTTPS and that no auth tokens are exposed in the page source.

7. Deployment & Environment

Required Environment Variables

VariablePurpose
DATABASE_URLPostgreSQL connection string
STRIPE_SECRET_KEYPlatform Stripe secret key
STRIPE_WEBHOOK_SECRETFor verifying Stripe webhook signatures
GOOGLE_CLIENT_IDOAuth client ID
GOOGLE_CLIENT_SECRETOAuth client secret
SENDGRID_API_KEYOr AWS SES credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
FINGERPRINTJS_SECRET_KEYServer-side verification key
FINGERPRINTJS_PUBLIC_KEYClient-side public key

8. Where to Start

Recommended build order to minimize rework:

  1. Browse all pages at https://secure.signpaygo.com/signpaygo-dashboard.html — click through every interaction before writing a line of code.
  2. Set up the ASP.NET MVC project, PostgreSQL connection, and EF Core migrations.
  3. Implement Auth: ASP.NET Core Identity + Google Sign-In.
  4. Build Dashboard and Events CRUD (create, list, detail).
  5. Implement the waiver editor — server-side save/load of events.waiver_content.
  6. Port Past Templates from PastTemplates.js to the waiver_templates table.
  7. Implement Stripe Connect onboarding flow (Settings page).
  8. Build the public parent-facing sign + pay page (/sign/{slug}).
  9. Implement PDF generation on waiver submission — generate, store to cloud, save path to participants.
  10. Wire up email sending: invitations, reminders, receipts (attach signed PDF to receipt email).
  11. Integrate FingerprintJS on the signing page.
  12. QA pass — verify API & Webhooks section is CSS-hidden in Settings / Integrations.
  13. Deploy to staging and run an end-to-end test of the full parent sign + pay flow.

Questions? Contact the product team before making architectural decisions that deviate from this brief.
SignPayGo — Confidential — Internal Use Only