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
- As a community member, I want to start a typed onboarding journey, so that I can become eligible for what it unlocks.
- As a person, I want my onboarding progress persisted per step, so that I can pause and resume exactly where I left off.
- As a person doing the
Squads onboarding, I want the Welcome onboarding required first, so that squad entry builds on community entry.
- 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.
- 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.
- As a person, I want my approved challenge PR to complete my onboarding automatically, so that I become APTO without manual intervention.
- As a person, I want to see which step I'm on and what's left, so that I understand my path.
- As a reviewer, I want to curate the challenge entirely on GitHub (approve / request changes), so that I don't need a second tool.
- 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.
- 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.
- 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.
- 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.
- 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.
- As an operator, I want every step transition auditable (when it started/finished, retries), so that I can see how someone progressed.
- As a person, I want to be able to pause my journey, so that life can happen without losing progress.
- As the platform, I want onboarding state scoped per tenant, so that the same person can be onboarded independently across tenants.
- 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_login → User 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)
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
onboardingdomain module that owns polymorphic onboarding state machines, one perOnboardingType(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. TheSquadstype's completion is the domain shorthand APTO, which unlocks squad candidacy/creation in thesquadsmodule.A person's
Squadsjourney is: completeWelcome(prerequisite) → submitform(auto-advances, no curation) → [gate: GitHub account linked] →git_challenge(open a PR on achallengerepo; a human reviewer approves it on GitHub) → completed = APTO. The challenge approval reaches the platform via a new domain event emitted byintegration-github, keeping all GitHub transport in one place.User Stories
Squadsonboarding, I want theWelcomeonboarding required first, so that squad entry builds on community entry.squadsstay decoupled.integration-github, so that no second component talks to GitHub.git_challengestep blocked with a clear CTA, so that I link first and the approval always matches a user.OnboardingTypeto be a new enum case + a handler class, so that the model and consumers don't change.Implementation Decisions
Module: new domain module
onboarding(namespaceHe4rt\Onboarding). Tenant-scoped throughout. ServiceProvider atsrc/OnboardingServiceProvider.php(scaffold currently lives insrc/Providers/— move it).Polymorphism (enum → handler):
OnboardingType(enum) resolves behaviour viahandler(): OnboardingFlow— same idiom asIdentityProvider::getClient(). TheOnboardingFlowcontract declaressteps(),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); columnsstatus(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).OnboardingType,OnboardingStatus,OnboardingStepStatus. Backed-string, cast on models;@propertyPHPDoc per.ai/04-model-phpdoc-sync.Gate: a simple interface
isCompleted(user, tenant, OnboardingType)plus prerequisite enforcement on start. This is the only surfacesquadsdepends on.Squads flow:
prerequisites() = [Welcome]; stepsform(auto-advances on submit, no curation) andgit_challenge. A gate (not a step) requires a linked GitHubExternalIdentitybeforegit_challenge; blocked otherwise with a CTA.isComplete()=git_challengedone.Challenge-approval signal (per ADR-0002):
integration-githubemits a new domain eventGithubPullRequestApproved(author_login,repo,pr_number,approved_at) when it seespull_request_reviewwithstate = approved.onboardingregisters a listener that resolvesauthor_login→UserviaExternalIdentity(providergithub) and advances thegit_challengestep. Because GitHub linking is a gate, the author always resolves — no reconciliation/buffer.Challenge repo (per ADR-0002):
GithubRepositorygains apurposecolumn (contributions|challenge).challengerepos do NOT emitGithubContributionRecorded(no XP).onboardingresolves the challenge repo viaGithubRepositorywherepurpose = challenge(tenant-scoped) and ignores approvals from non-challenge repos.Actions:
StartOnboarding,AdvanceStep,PauseOnboarding,ResumeOnboarding.Dependencies: depends on
identity(User, tenant, GitHubExternalIdentity) and listens tointegration-github. Never imports fromsquads.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_challengeblocked without a linked GitHub identity,isComplete()flips status tocompleted; prerequisite chain blocks startingSquadsuntilWelcomeis completed;isCompleted(user, tenant, type)returns the right gate value. Pause/resume preserves the current step.GithubPullRequestApprovedfor achallengerepo advances/completes the matching person'sgit_challenge; an approval from a non-challenge repo is ignored.Prior art: feature tests in sibling modules (
integration-githubwebhook/projection tests;identityaction tests) and the Filament/Pest patterns in the repo. Use factories with->recycle($tenant).Out of Scope
squadsmodule itself (separate PRD) — this PRD only exposes the gate.panel-app/panel-adminand can be a follow-up.Further Notes
purposeflag — minor, to discuss; current ADR keepspurpose.Welcomeonboarding's concrete steps are intentionally light here; define them when that flow is fleshed out.Subtarefas (tracer bullets — ordem de dependência)