Skip to content

feat(database): Wave 1 authz MVP storage (#422) - #677

Merged
adlerhurst merged 12 commits into
cursor/permission-storage-wave0-6e44from
cursor/permission-storage-wave1-3354
Aug 7, 2026
Merged

feat(database): Wave 1 authz MVP storage (#422)#677
adlerhurst merged 12 commits into
cursor/permission-storage-wave0-6e44from
cursor/permission-storage-wave1-3354

Conversation

@adlerhurst

@adlerhurst adlerhurst commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Wave 1 authz MVP storage on top of Wave 0 (#666): Goose migrations for the six tables, dual-write resource-scope identifiers + membership edges from existing project/team/user/membership statements, and PersistCatalogVersion for compiler CatalogMutations (#720). SQLite gets the same schema and dual-write path for local parity.

Review / maintainability follow-ups:

  • Membership-edge deletes use one DeleteAuthzMembershipEdges(filter) API (kept ForTeamDeactivate for the subquery).
  • Shared dialect/authz holds multi-write user helpers, MembershipEdgeSchema, and catalog row builders (engines keep SQL). Removed the three identical dual-write adapter files and identity wrappers.
  • Typed authz discriminators (including DDL-aligned principal types), assignment scope constructors, and AuthzMembershipEdgeKey.
  • Spanner activeUniqueKey lives in Spanner storage (not domain).
  • Spanner membership-edge upsert uses a created_at no-op update (PK updates rejected).
  • Authz enum SQL parameters bind via .String() (matching MembershipStatus), including sibling ResourceKind/CatalogKind binds.
  • Spanner nullable authz params bind via spannerNullString / spannerNullTime (not Go pointers).
  • CreateAuthzAssignment mints empty IDs via ensureManagedID (PrefixAuthzAssignment / asgn_).
  • GetAuthzCatalog loads a persisted catalog + projected rows; deep Persist round-trip asserts live in stmttest (all dialects). Seeded viewer→admin closure depth is 2; Spanner relation-reference columns have DEFAULTs.
  • authz_membership_edges has Wave 0 set FK (project_id, set_id) → teams + MVP CHECK (user∈team) with ON DELETE CASCADE on all dialects (Spanner matched to Postgres/SQLite). stmttest DeleteTeamCascadesEdges asserts hard-deleting a team removes edges on every engine.

Validation

go generate ./internal/service/
go test ./internal/domain/ ./internal/service/ ./internal/storage/v2/dialect/authz/ ./internal/storage/v2/dialect/spanner/ -run ActiveUniqueKey
go test -tags sqlite_integration ./internal/storage/v2/stmttest/ -run 'Authz|PersistCatalog|DualWrite'
go test -tags postgres_integration ./internal/storage/v2/stmttest/ ./internal/storage/v2/dialect/postgres/ -run 'Authz|PersistCatalog|CatalogSeed|DualWrite'

Postgres + SQLite green (including DeleteTeamCascadesEdges). Spanner compile-checked + ActiveUniqueKey unit tests; full emulator coverage via CI server:test-spanner.

Release notes / changeset

Changeset already present: .changeset/authz-mvp-storage.md (@zitadel/server minor). Review/maintainability follow-ups are Go storage/API-shape refactors — no additional changeset.

Notes

Open in Web Open in Cursor 

Capture locked schema decisions (D1–D13), dual-write membership edges,
and a #333 cross-project grant depiction so #422 can implement migrations
without re-litigating the relational shape.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nextgen Ready Ready Preview Aug 7, 2026 2:45pm
nextgen-docs Ready Ready Preview Aug 7, 2026 2:45pm
nextgen-mock-zitadel Ready Ready Preview Aug 7, 2026 2:45pm

Request Review

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 7f69693

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@zitadel/server Minor
@zitadel/cli Minor
@zitadel/testing Minor
@zitadel/server-linux-x64 Minor
@zitadel/server-linux-arm64 Minor
@zitadel/server-darwin-x64 Minor
@zitadel/server-darwin-arm64 Minor
@zitadel/server-win32-x64 Minor
@zitadel/api Minor
@zitadel/config Minor
@zitadel/components Minor
@zitadel/sdk-core Minor
@zitadel/sdk-next Minor
@zitadel/sdk-nuxt Minor
@zitadel/sdk-react Minor
@zitadel/sdk-vue Minor
@zitadel/sdk-angular Minor
@zitadel/sdk-solid Minor
@zitadel/sdk-svelte Minor
@zitadel/sdk-qwik Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread internal/service/statement.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Wave 1 of the authz MVP storage layer across Postgres/Spanner/SQLite: adds Goose migrations for the authz tables, introduces statement interfaces + dialect implementations for catalog persistence/assignments/membership edges/resource-scope indexing, and wires same-transaction dual-write hooks from existing project/team/user/membership mutations. This aligns the DB persistence surface with the Wave 0 DDL decisions and the Wave 0.5 compiler output (compiler.CatalogMutations).

Changes:

  • Add authz MVP schema migrations (catalogs/relations/closure/references/expression_edges, assignments, membership edges, resource_scope_index) and seed cat_sys_1.
  • Add new statement interfaces + dialect implementations for PersistCatalogVersion, AuthzAssignment*, AuthzMembershipEdge*, and ResourceScope*.
  • Add/expand integration tests covering resource-scope dual-write and membership-edge projection across dialects, consolidating team-membership tests into stmttest.

Reviewed changes

Copilot reviewed 50 out of 51 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
internal/storage/v2/stmttest/team_membership_test.go Expands team membership statement coverage (list filters + update-not-found).
internal/storage/v2/stmttest/authz_test.go Adds cross-dialect CRUD tests for authz assignments and catalog persistence round-trip.
internal/storage/v2/stmttest/authz_resource_scope_test.go Adds tests for RSI upsert/get/delete and cascade behavior on project delete.
internal/storage/v2/stmttest/authz_membership_edge_test.go Adds cross-dialect tests for membership edge upsert/get/list/delete shapes.
internal/storage/v2/stmttest/authz_dual_write_test.go Tests dual-write hooks from user/team/team-membership lifecycle events into RSI/edges.
internal/storage/v2/dialect/sqlite/user.go Wires same-tx RSI + membership-edge dual writes on user create/deactivate/delete.
internal/storage/v2/dialect/sqlite/team.go Wraps create/deactivate in tx; dual-writes RSI and clears membership edges on deactivate.
internal/storage/v2/dialect/sqlite/team_membership.go Wraps create/update in tx; projects roster status changes into authz membership edges.
internal/storage/v2/dialect/sqlite/statement.go Registers new statement implementations (RSI/authz assignment/edge/catalog).
internal/storage/v2/dialect/sqlite/resource_scope_index.go Implements SQLite RSI upsert/get/delete statements.
internal/storage/v2/dialect/sqlite/project.go Dual-writes RSI on project create (transactional).
internal/storage/v2/dialect/sqlite/migration/sql/000002_authz_mvp.sql Adds SQLite authz MVP schema + seed + backfill.
internal/storage/v2/dialect/sqlite/authz_membership_edge.go Implements SQLite authz membership edge statements (upsert/get/list/delete).
internal/storage/v2/dialect/sqlite/authz_catalog.go Implements SQLite PersistCatalogVersion mapping compiler mutations into rows.
internal/storage/v2/dialect/sqlite/authz_assignment.go Implements SQLite authz assignment CRUD statements.
internal/storage/v2/dialect/spanner/user.go Wires same-tx RSI + membership-edge dual writes on user create/deactivate/delete.
internal/storage/v2/dialect/spanner/team.go Wraps create/deactivate in tx; dual-writes RSI and clears membership edges on deactivate.
internal/storage/v2/dialect/spanner/team_membership.go Wraps create/update in tx; projects roster status changes into membership edges.
internal/storage/v2/dialect/spanner/team_membership_test.go Removes dialect-local test (superseded by stmttest coverage).
internal/storage/v2/dialect/spanner/statement.go Registers new statement implementations (RSI/authz assignment/edge/catalog).
internal/storage/v2/dialect/spanner/resource_scope_index.go Implements Spanner RSI upsert/get/delete statements.
internal/storage/v2/dialect/spanner/project.go Dual-writes RSI on project create; clarifies RSI cascade on delete.
internal/storage/v2/dialect/spanner/migration/sql/000018_authz_mvp.sql Adds Spanner authz MVP schema + seed + backfill (incl. NULL_FILTERED uniqueness).
internal/storage/v2/dialect/spanner/authz_membership_edge.go Implements Spanner authz membership edge statements (upsert/get/list/delete).
internal/storage/v2/dialect/spanner/authz_catalog.go Implements Spanner PersistCatalogVersion with active-guard retirement semantics.
internal/storage/v2/dialect/spanner/authz_catalog_seed_test.go Adds Spanner integration test asserting seeded system catalog invariants.
internal/storage/v2/dialect/spanner/authz_assignment.go Implements Spanner authz assignment CRUD and active-unique-key behavior.
internal/storage/v2/dialect/spanner/authz_assignment_key_test.go Unit-tests Spanner activeUniqueKey key construction semantics.
internal/storage/v2/dialect/postgres/user.go Wires same-tx RSI + membership-edge dual writes on user create/deactivate/delete.
internal/storage/v2/dialect/postgres/team.go Wraps create/deactivate in tx; dual-writes RSI and clears membership edges on deactivate.
internal/storage/v2/dialect/postgres/team_membership.go Wraps create/update in tx; projects roster status changes into membership edges.
internal/storage/v2/dialect/postgres/team_membership_test.go Removes dialect-local test (superseded by stmttest coverage).
internal/storage/v2/dialect/postgres/statement.go Registers new statement implementations (RSI/authz assignment/edge/catalog).
internal/storage/v2/dialect/postgres/resource_scope_index.go Implements Postgres RSI upsert/get/delete statements.
internal/storage/v2/dialect/postgres/project.go Dual-writes RSI on project create; wraps delete error.
internal/storage/v2/dialect/postgres/migration/sql/000015_authz_mvp.sql Adds Postgres authz MVP schema + seed + backfill.
internal/storage/v2/dialect/postgres/authz_membership_edge.go Implements Postgres authz membership edge statements (upsert/get/list/delete).
internal/storage/v2/dialect/postgres/authz_catalog.go Implements Postgres PersistCatalogVersion mapping compiler mutations into rows.
internal/storage/v2/dialect/postgres/authz_catalog_seed_test.go Adds Postgres integration test asserting seeded system catalog invariants.
internal/storage/v2/dialect/postgres/authz_catalog_persist_test.go Adds Postgres integration tests for persistence mapping + catalog retirement behavior.
internal/storage/v2/dialect/postgres/authz_assignment.go Implements Postgres authz assignment CRUD statements.
internal/storage/v2/dialect/authz/membership_edge_schema.go Adds shared filter schema for membership edge deletes across dialects.
internal/storage/v2/dialect/authz/dual_write.go Adds shared dual-write helpers for user lifecycle → RSI/edges projection.
internal/storage/v2/dialect/authz/catalog_rows.go Adds shared mapping from compiler mutations to typed storage rows.
internal/service/statement.go Extends statement interfaces and adds SyncUserTeamMembershipEdge helper + catalog interface.
internal/domain/team_membership.go Adds MembershipStatus.IsAuthzActive() helper for projection rules.
internal/domain/authz.go Adds domain types/constants for RSI/authz tables and constructors for scopes/edges.
internal/authz/compiler/output.go Minor import formatting change.
docs/design/api/permission-storage.md Updates design doc from Wave 0 spike to Wave 1 implemented details.
.changeset/authz-mvp-storage.md Adds changeset for @zitadel/server minor release of authz MVP storage.
Files not reviewed (1)
  • internal/service/mocks/statement.mock.go: Generated file
Suppressed comments (5)

internal/storage/v2/dialect/postgres/authz_membership_edge.go:70

  • Stringify key SetType/MemberType when using them as SQL parameters to keep enum bindings consistent with the rest of the dialect (and avoid pgx encoding surprises with named string types).
	rows, err := s.client.Query(ctx, getAuthzMembershipEdgeStmt, key.ProjectID, key.SetType, key.SetID, key.MemberType, key.MemberID)

internal/storage/v2/dialect/postgres/authz_membership_edge.go:83

  • Stringify memberType when binding it as a SQL parameter (consistent with other statement methods that pass enums as strings).
	rows, err := s.client.Query(ctx, listAuthzMembershipEdgesByMemberStmt, projectID, memberType, memberID)

internal/storage/v2/dialect/sqlite/authz_assignment.go:100

  • Bind principalType as principalType.String() to keep enum parameter binding consistent with other statement code paths.
    internal/storage/v2/dialect/sqlite/authz_membership_edge.go:71
  • Bind key SetType/MemberType as strings when using them as query parameters (consistent enum binding).
    internal/storage/v2/dialect/sqlite/authz_membership_edge.go:85
  • Bind memberType as memberType.String() when using it as a SQL parameter to keep enum bindings consistent across dialects.

Comment thread internal/storage/v2/dialect/postgres/authz_assignment.go Outdated
Comment thread internal/storage/v2/dialect/postgres/authz_membership_edge.go Outdated
Comment thread internal/storage/v2/dialect/spanner/authz_assignment.go Outdated
Comment thread internal/storage/v2/dialect/spanner/authz_assignment.go Outdated
Comment thread internal/storage/v2/dialect/spanner/authz_membership_edge.go Outdated
Comment thread internal/storage/v2/dialect/spanner/authz_membership_edge.go Outdated
Comment thread internal/storage/v2/dialect/sqlite/authz_assignment.go Outdated
Comment thread internal/storage/v2/dialect/sqlite/authz_membership_edge.go Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 51 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • internal/service/mocks/statement.mock.go: Generated file

Comment thread internal/storage/v2/dialect/spanner/resource_scope_index.go
Comment thread internal/storage/v2/dialect/spanner/authz_assignment.go
Comment thread internal/storage/v2/dialect/spanner/authz_catalog.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 51 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • internal/service/mocks/statement.mock.go: Generated file

@livio-a livio-a left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Wave 1 authz MVP storage — reviewed as part of the stack (#666#677#758).

Overall this is a carefully built, high-consistency change. I verified the riskiest pieces by hand: the dual-write (team_memberships + authz_membership_edges + resource_scope_index) is wrapped in the same withTransaction on every create/update/delete/deactivate path across all three dialects, so those rows can't diverge; placeholder/column counts match the DDL exactly; everything is parameterized (no injection surface); table/index parity across Postgres/Spanner/SQLite is clean; and the Spanner active_unique_key stand-in for the Postgres partial-unique index is genuinely equivalent (same tuple order, \x1f-joined, nulled on revoke). No blocker- or high-severity defect.

Worth fixing before merge

[MEDIUM] CreateAuthzAssignment doesn't mint id via ensureManagedID.
internal/storage/v2/dialect/{postgres,spanner,sqlite}/authz_assignment.go insert a.ID verbatim, whereas CreateProject/CreateTeam/CreateUser all open with ensureManagedID(&x.ID, domain.PrefixX). There's no PrefixAuthzAssignment in internal/domain. This deviates from ADR 047 / internal/storage/v2/AGENTS.md ("assign empty IDs with dialect Ensure/ensureManagedID before INSERT; don't invent IDs in domain/service/handlers"). A future grants-API caller that follows the established pattern (leave ID empty, let the dialect mint it) will hit CHECK (id <> '') instead of a minted ID. Suggest adding PrefixAuthzAssignment and minting in the dialect statement.

[MEDIUM] Catalog-persistence correctness is deep-tested only on Postgres.
Only postgres/authz_catalog_persist_test.go reads back the projected authz_relations / authz_relation_references / authz_expression_edges / authz_relation_closure rows and verifies retire-previous. There's no spanner/ or sqlite/ equivalent (confirmed absent), and the shared stmttest persist test only asserts the call succeeds. A Spanner/SQLite bug that mis-writes closure depth or drops references would pass CI. Given how meticulously the DDL is mirrored across dialects, the correctness verification should be too.

Lower severity

  • [LOW] Seed closure depth drift. The hand-seeded viewer→admin closure row is written at depth 1, but the model chain is viewer→editor→admin, so the compiler BFS emits depth 2. No Wave 1 runtime impact (resolver joins from/to only, ignores depth), but it's a silent landmine for when #420 recompiles cat_sys_1 through PersistCatalogVersion. Note the depth-invariant test in #758 can't catch this — its oracle re-implements the same BFS.
  • [LOW] Spanner authz_relation_references omits DEFAULTs. ref_relation/wildcard/condition are NOT NULL with no DEFAULT, whereas Postgres/SQLite give DEFAULT ''/DEFAULT false. An INSERT omitting those columns succeeds on PG/SQLite but hits NOT-NULL on Spanner. Latent (mapper always supplies all columns) but a real DDL parity gap.
  • [LOW] authz_relations.kind hardcoded to 'relation' in dialect/authz/catalog_rows.go (the compiler carries no kind), so 'permission' — allowed by the CHECK — can never be produced. Fine for MVP; worth a comment/follow-up.
  • [LOW] Team FKs use ON DELETE CASCADE on resource_scope_index/authz_assignments, diverging from the deliberate RESTRICT/NO ACTION on team_memberships (ADR 024: deletion via lifecycle service, not raw cascade). Internally consistent across dialects — flagging so it's a conscious choice, not drift.
  • [LOW] SQLite project-delete cascade depends on PRAGMA foreign_keys = ON. DeleteProjectByID now relies on FK cascade to clean RSI/edges/assignments; correct on PG/Spanner, silently orphans on SQLite without the pragma. Pre-existing global assumption, now extended to the authz tables — worth confirming the pool enforces it.
  • [NIT] PG/SQLite membership-edge upsert uses a no-op PK self-assignment (DO UPDATE SET project_id = EXCLUDED.project_id) to preserve created_at; the Spanner variant documents why (created_at = created_at) but PG/SQLite don't.

Test coverage (relocation verified, minor gaps)

The removed team_membership_test.go coverage was relocated and expanded into the shared stmttest suite (CRUD/FK/not-found now run on all three dialects; dual-write status variants moved to authz_dual_write_test.go and grew Deactivate_clears... cases) — no net loss. Remaining gaps, all LOW: membership-edge/dual-write tests only exercise user+team (no agent-member or app-set cases); and nothing asserts authz_assignments/edges are cascade-cleaned on project delete (only RSI is checked).

Align check SQL with D13 via principal_home_project_id, single-source
catalog tables (D14), tighter DDL CHECKs, and supersede authz.md /
ADR follow-ups that still pointed at team_memberships checks and
delegation/expression-edge tables.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

Thanks for the review — addressed the merge-blocking items and the clear LOWs in 6513a0d1:

MEDIUM

  • CreateAuthzAssignment now mints empty IDs via ensureManagedID + PrefixAuthzAssignment (asgn_), registered in ADR 047. stmttest covers empty-ID minting.
  • Catalog persist deep coverage moved to stmttest via new GetAuthzCatalog (domain snapshot of metadata + relations/refs/edges/closure) on all three dialects. Deleted the Postgres-only dialect SQL persist test (ADR-041). Seed tests stay dialect-local.

LOW (fixed)

  • Seeded viewer→admin closure depth corrected to 2 (all three Goose seeds); Postgres seed test asserts it.
  • Spanner authz_relation_references now has DEFAULT for ref_relation / wildcard / condition.
  • Comments: hardcoded 'relation' kind in catalog_rows.go; PG/SQLite membership-edge PK no-op upsert (preserve created_at).

LOW (acknowledged, no code)

  • Team FKs ON DELETE CASCADE on RSI/assignments: conscious MVP choice; lifecycle still owns team delete (not ADR-024 drift).
  • SQLite PRAGMA foreign_keys: already enforced by the pool DSN (foreign_keys(1) in sqlite/dialect.go).
  • Agent-member / app-set dual-write gaps and project-delete cascade coverage for assignments/edges: deferred follow-ups.

@livio-a livio-a left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authz_membership_edges — doc/migration mismatch on the set FK. #666's Wave 0 DDL now declares FOREIGN KEY (project_id, set_id) REFERENCES teams (project_id, id) on authz_membership_edges, but the migration here (000015/000018/000002) only has the project_id → projects FK. Since Wave 0 is the frozen schema, the migration should match it — and adding this FK is the DB backstop that otherwise leaves set_id free-floating (an edge could reference a non-existent team, caught only by dual-write discipline). Worth adding across all three dialects (Postgres/SQLite REFERENCES teams(project_id, id), Spanner ... ON DELETE NO ACTION per convention). If instead this is meant to stay app-enforced for MVP, drop the line from the Wave 0 doc so the two agree.

@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Addressed in 48d77364: authz_membership_edges now has the Wave 0 set FK (project_id, set_id) → teams plus MVP CHECK (member_type = 'user' AND set_type = 'team') on Postgres/SQLite (ON DELETE CASCADE) and Spanner (ON DELETE NO ACTION, matching team_memberships). Design doc snippet aligned.

TDD: MissingSetReturnsForeignKeyError failed pre-DDL (upsert succeeded), then passed on sqlite + postgres after the FK. Existing membership/dual-write suite still green.

cursoragent and others added 6 commits August 7, 2026 08:05
Re-publish the Wave 1 tree as a linear commit on
cursor/permission-storage-wave0-6e44 so GitHub mergeability
clears for stacked PR #677 (local FF was already clean).

Prior tip (merge history): 63351f7

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Resolve team_membership.go by keeping both IsAuthzActive (Wave 1
authz dual-write) and RosterMembershipStatuses (main roster reads).

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
…rage-wave0-6e44

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
…6e44' into cursor/permission-storage-wave1-3354

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Add stmttest DeleteTeamCascadesEdges so every dialect asserts that
hard-deleting a team removes membership edges. Spanner set FK was
NO ACTION while Postgres/SQLite used CASCADE; align Spanner to CASCADE
and document that CASCADE is not translated to NO ACTION.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Resolve permission-storage.md and ADR 032 after Wave 0 force-push
(main sync): keep Wave 1 shipped schema (expression edges / typed
relations / CASCADE), fold Wave 0 MATCH SIMPLE + RSI delete notes and
system-permission-catalog see-also link. Skip grantor_type null CHECK
(not in Wave 1 migrations). ADR 032 keeps Wave 1 D5/D14 supersession.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants