Skip to content

prd(onboarding): polymorphic entry layer + APTO gate #341

Description

@danielhe4rt

Synthesized from a /grill-with-docs session. Design is recorded in app-modules/onboarding/CONTEXT.md and ADRs 0001-onboarding-polimorfico-por-tipo and 0002-sinal-de-pr-aprovado-via-evento-de-dominio. This PRD implements those decisions. The sibling PRD prd(squads) consumes this module's completion gate.

Problem Statement

People enter the He4rt ecosystem through informal channels with no consistent screening. There is no way to know who has actually been vetted ("is this person allowed to apply to / propose a squad?"), and the only screening idea on the table (a form + a Git challenge) is tied to one specific use case. As the community grows, we need a universal, mandatory entry gate that is reusable across contexts (community entry today, squad entry now, others later), each with its own steps and payload — without rebuilding the machinery each time.

Solution

A new onboarding domain module that owns polymorphic onboarding state machines, one per OnboardingType (Welcome, Squads, …). A person walks a typed flow made of auditable steps; when they complete a type, that completion becomes a gate other modules read. The Squads type's completion is the domain shorthand APTO, which unlocks squad candidacy/creation in the squads module.

A person's Squads journey is: complete Welcome (prerequisite) → submit form (auto-advances, no curation) → [gate: GitHub account linked] → git_challenge (open a PR on a challenge repo; a human reviewer approves it on GitHub) → completed = APTO. The challenge approval reaches the platform via a new domain event emitted by integration-github, keeping all GitHub transport in one place.

User Stories

  1. As a community member, I want to start a typed onboarding journey, so that I can become eligible for what it unlocks.
  2. As a person, I want my onboarding progress persisted per step, so that I can pause and resume exactly where I left off.
  3. As a person doing the Squads onboarding, I want the Welcome onboarding required first, so that squad entry builds on community entry.
  4. As a person, I want the form step to advance automatically on submit, so that I'm not blocked waiting on curation for the easy part.
  5. As a person, I want to be told to link my GitHub account before the challenge, so that my approved PR can be matched to me.
  6. As a person, I want my approved challenge PR to complete my onboarding automatically, so that I become APTO without manual intervention.
  7. As a person, I want to see which step I'm on and what's left, so that I understand my path.
  8. As a reviewer, I want to curate the challenge entirely on GitHub (approve / request changes), so that I don't need a second tool.
  9. As the platform, I want each onboarding type to define its own steps, prerequisites, payload contract and completion rule, so that adding a new type doesn't touch existing consumers.
  10. As the platform, I want a single, simple gate query ("did this person complete onboarding type X in this tenant?"), so that consumers like squads stay decoupled.
  11. As the platform, I want the challenge-approval signal to arrive as a domain event from integration-github, so that no second component talks to GitHub.
  12. As the platform, I want challenge repos flagged so they do NOT award gamification XP, so that doing the challenge isn't double-counted as a contribution.
  13. As a person without a linked GitHub account, I want the git_challenge step blocked with a clear CTA, so that I link first and the approval always matches a user.
  14. As an operator, I want every step transition auditable (when it started/finished, retries), so that I can see how someone progressed.
  15. As a person, I want to be able to pause my journey, so that life can happen without losing progress.
  16. As the platform, I want onboarding state scoped per tenant, so that the same person can be onboarded independently across tenants.
  17. As a future maintainer, I want a new OnboardingType to be a new enum case + a handler class, so that the model and consumers don't change.

Implementation Decisions

Module: new domain module onboarding (namespace He4rt\Onboarding). Tenant-scoped throughout. ServiceProvider at src/OnboardingServiceProvider.php (scaffold currently lives in src/Providers/ — move it).

Polymorphism (enum → handler): OnboardingType (enum) resolves behaviour via handler(): OnboardingFlow — same idiom as IdentityProvider::getClient(). The OnboardingFlow contract declares steps(), prerequisites(), advance(), isComplete(). All type-specific rules live in the handler; consumers never reference concrete types.

Persistence (per ADR-0001): one polymorphic model + a step table.

  • onboardings: one row per (tenant_id, user_id, type); columns status (in_progress/paused/completed/rejected), completed_at, paused_at. UNIQUE (tenant_id, user_id, type).
  • onboarding_steps: one row per step; step_key, status, data (jsonb), completed_at. UNIQUE (onboarding_id, step_key).
  • Enums: OnboardingType, OnboardingStatus, OnboardingStepStatus. Backed-string, cast on models; @property PHPDoc per .ai/04-model-phpdoc-sync.

Gate: a simple interface isCompleted(user, tenant, OnboardingType) plus prerequisite enforcement on start. This is the only surface squads depends on.

Squads flow: prerequisites() = [Welcome]; steps form (auto-advances on submit, no curation) and git_challenge. A gate (not a step) requires a linked GitHub ExternalIdentity before git_challenge; blocked otherwise with a CTA. isComplete() = git_challenge done.

Challenge-approval signal (per ADR-0002): integration-github emits a new domain event GithubPullRequestApproved (author_login, repo, pr_number, approved_at) when it sees pull_request_review with state = approved. onboarding registers a listener that resolves author_loginUser via ExternalIdentity (provider github) and advances the git_challenge step. Because GitHub linking is a gate, the author always resolves — no reconciliation/buffer.

Challenge repo (per ADR-0002): GithubRepository gains a purpose column (contributions | challenge). challenge repos do NOT emit GithubContributionRecorded (no XP). onboarding resolves the challenge repo via GithubRepository where purpose = challenge (tenant-scoped) and ignores approvals from non-challenge repos.

Actions: StartOnboarding, AdvanceStep, PauseOnboarding, ResumeOnboarding.

Dependencies: depends on identity (User, tenant, GitHub ExternalIdentity) and listens to integration-github. Never imports from squads.

Testing Decisions

Good tests assert external behaviour, not internals: drive a flow through its public Actions/handler and assert the resulting state (status, completed step, gate result), not private methods.

  • OnboardingFlow + gate (confirmed in scope): per-type state machine — advance() moves to the right step, auto-advance on form submit, git_challenge blocked without a linked GitHub identity, isComplete() flips status to completed; prerequisite chain blocks starting Squads until Welcome is completed; isCompleted(user, tenant, type) returns the right gate value. Pause/resume preserves the current step.
  • Listener reconciliation path: a GithubPullRequestApproved for a challenge repo advances/completes the matching person's git_challenge; an approval from a non-challenge repo is ignored.

Prior art: feature tests in sibling modules (integration-github webhook/projection tests; identity action tests) and the Filament/Pest patterns in the repo. Use factories with ->recycle($tenant).

Out of Scope

  • The squads module itself (separate PRD) — this PRD only exposes the gate.
  • Conducting the challenge review on-platform — curation stays on GitHub.
  • The "núcleo do jogo" (all-or-nothing project, scoring, season, redemption).
  • Reconciliation of approvals for unlinked GitHub accounts (eliminated by the linking gate).
  • UI polish; presentation lives in panel-app/panel-admin and can be a follow-up.

Further Notes

  • ADR-0002 diverges from the P.O. doc's original BDD (the "approval with unlinked GitHub account → retain & reconcile" scenario is removed). The P.O. doc should be updated; a reviewer (stherzada) also asked for that doc to be generated.
  • Reviewer raised whether challenge-repo matching could be "by name" rather than a purpose flag — minor, to discuss; current ADR keeps purpose.
  • Welcome onboarding's concrete steps are intentionally light here; define them when that flow is fleshed out.

Subtarefas (tracer bullets — ordem de dependência)

Metadata

Metadata

Assignees

No one assigned

    Labels

    difficulty:hard1-2 weeksmod:integration-githubGitHub integration modulemod:onboardingOnboarding entry layer (pre-triage, APTO gate)ready-for-agentFully specified, ready for an AFK agenttype:prdProduct Requirements Document

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions