diff --git a/internal/check/engine.go b/internal/check/engine.go
index 2e232b839..f8205b92a 100644
--- a/internal/check/engine.go
+++ b/internal/check/engine.go
@@ -35,104 +35,74 @@ type (
 	Query         = relationtuple.RelationQuery
 )
 
+const WildcardRelation = "..."
+
 func NewEngine(d EngineDependencies) *Engine {
 	return &Engine{
 		d: d,
 	}
 }
 
-func (e *Engine) isIncluded(
-	ctx context.Context,
-	requested *RelationTuple,
-	rels []*RelationTuple,
-	restDepth int,
-) checkgroup.CheckFunc {
-	if restDepth < 0 {
-		e.d.Logger().Debug("reached max-depth, therefore this query will not be further expanded")
-		return checkgroup.UnknownMemberFunc
+// CheckIsMember checks if the relation tuple's subject has the relation on the
+// object in the namespace either directly or indirectly and returns a boolean
+// result.
+func (e *Engine) CheckIsMember(ctx context.Context, r *RelationTuple, restDepth int) (bool, error) {
+	result := e.CheckRelationTuple(ctx, r, restDepth)
+	if result.Err != nil {
+		return false, result.Err
 	}
+	return result.Membership == checkgroup.IsMember, nil
+}
 
-	// This is the same as the graph problem "can requested.Subject be reached
-	// from requested.Object through the first outgoing edge requested.Relation"
-	//
-	// We implement recursive depth-first search here.
-	// TODO replace by more performant algorithm:
-	// https://github.com/ory/keto/issues/483
-
-	ctx = graph.InitVisited(ctx)
-	g := checkgroup.New(ctx)
-
-	for _, sr := range rels {
-		var wasAlreadyVisited bool
-		ctx, wasAlreadyVisited = graph.CheckAndAddVisited(ctx, sr.Subject)
-		if wasAlreadyVisited {
-			continue
-		}
-
-		// we only have to check Subject here as we know that sr was reached
-		// from requested.ObjectID, requested.Relation through 0...n
-		// indirections
-		if requested.Subject.Equals(sr.Subject) {
-			// found the requested relation
-			g.SetIsMember()
-			break
-		}
-
-		sub, isSubjectSet := sr.Subject.(*relationtuple.SubjectSet)
-		if !isSubjectSet {
-			continue
-		}
+// CheckRelationTuple checks if the relation tuple's subject has the relation on
+// the object in the namespace either directly or indirectly and returns a check
+// result.
+func (e *Engine) CheckRelationTuple(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.Result {
+	// global max-depth takes precedence when it is the lesser or if the request
+	// max-depth is less than or equal to 0
+	if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth {
+		restDepth = globalMaxDepth
+	}
 
-		g.Add(e.subQuery(
-			requested,
-			&Query{Object: sub.Object, Relation: sub.Relation, Namespace: sub.Namespace},
-			restDepth,
-		))
+	resultCh := make(chan checkgroup.Result)
+	go e.checkIsAllowed(ctx, r, restDepth)(ctx, resultCh)
+	select {
+	case result := <-resultCh:
+		return result
+	case <-ctx.Done():
+		return checkgroup.Result{Err: errors.WithStack(ctx.Err())}
 	}
-	return checkgroup.WithEdge(checkgroup.Edge{
-		Tuple: *requested,
-		Type:  expand.Union,
-	}, g.CheckFunc())
 }
 
-func (e *Engine) subQuery(
-	requested *RelationTuple,
-	query *Query,
-	restDepth int,
-) checkgroup.CheckFunc {
+// checkExpandSubject checks the expansions of the subject set of the tuple.
+//
+// For a relation tuple n:obj#rel@user, checkExpandSubject first queries for all
+// subjects that match n:obj#rel@* (arbirary subjects), and then for each
+// subject checks subject@user.
+func (e *Engine) checkExpandSubject(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
 	if restDepth < 0 {
 		e.d.Logger().
-			WithFields(requested.ToLoggerFields()).
+			WithFields(r.ToLoggerFields()).
 			Debug("reached max-depth, therefore this query will not be further expanded")
 		return checkgroup.UnknownMemberFunc
 	}
-
 	return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
 		e.d.Logger().
-			WithField("request", requested.String()).
-			WithField("query", query.String()).
-			Trace("check one indirection further")
-
-		// an empty page token denotes the first page (as tokens are opaque)
-		var prevPage string
-
-		// Special case: check if we can find the subject id directly
-		if rels, _, err := e.d.RelationTupleManager().GetRelationTuples(ctx, requested.ToQuery()); err == nil && len(rels) > 0 {
-			resultCh <- checkgroup.Result{
-				Membership: checkgroup.IsMember,
-				Tree: &expand.Tree{
-					Type:  expand.Leaf,
-					Tuple: requested,
-				},
-			}
-			return
-		}
+			WithField("request", r.String()).
+			Trace("check expand subject")
 
 		g := checkgroup.New(ctx)
 
+		var (
+			subjects  []*RelationTuple
+			pageToken string
+			err       error
+			visited   bool
+			innerCtx  = graph.InitVisited(ctx)
+			query     = &Query{Namespace: r.Namespace, Object: r.Object, Relation: r.Relation}
+		)
 		for {
-			nextRels, nextPage, err := e.d.RelationTupleManager().GetRelationTuples(ctx, query, x.WithToken(prevPage))
-			// herodot.ErrNotFound occurs when the namespace is unknown
+			subjects, pageToken, err = e.d.RelationTupleManager().GetRelationTuples(innerCtx, query, x.WithToken(pageToken))
 			if errors.Is(err, herodot.ErrNotFound) {
 				g.Add(checkgroup.NotMemberFunc)
 				break
@@ -140,48 +110,71 @@ func (e *Engine) subQuery(
 				g.Add(checkgroup.ErrorFunc(err))
 				break
 			}
-
-			g.Add(e.isIncluded(ctx, requested, nextRels, restDepth-1))
-
-			// loop through pages until either allowed, end of pages, or an error occurred
-			if nextPage == "" || g.Done() {
+			for _, s := range subjects {
+				innerCtx, visited = graph.CheckAndAddVisited(innerCtx, s.Subject)
+				if visited {
+					continue
+				}
+				if s.Subject.SubjectSet() == nil || s.Subject.SubjectSet().Relation == WildcardRelation {
+					continue
+				}
+				g.Add(e.checkIsAllowed(
+					innerCtx,
+					&RelationTuple{
+						Namespace: s.Subject.SubjectSet().Namespace,
+						Object:    s.Subject.SubjectSet().Object,
+						Relation:  s.Subject.SubjectSet().Relation,
+						Subject:   r.Subject,
+					},
+					restDepth-1,
+				))
+			}
+			if pageToken == "" || g.Done() {
 				break
 			}
-			prevPage = nextPage
 		}
 
 		resultCh <- g.Result()
 	}
 }
 
-func (e *Engine) CheckIsMember(ctx context.Context, r *RelationTuple, restDepth int) (bool, error) {
-	result := e.CheckRelationTuple(ctx, r, restDepth)
-	if result.Err != nil {
-		return false, result.Err
-	}
-	return result.Membership == checkgroup.IsMember, nil
-}
-
-func (e *Engine) CheckRelationTuple(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.Result {
-	// global max-depth takes precedence when it is the lesser or if the request
-	// max-depth is less than or equal to 0
-	if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth {
-		restDepth = globalMaxDepth
+// checkDirect checks if the relation tuple is in the database directly.
+func (e *Engine) checkDirect(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
+	if restDepth < 0 {
+		e.d.Logger().
+			WithField("method", "checkDirect").
+			Debug("reached max-depth, therefore this query will not be further expanded")
+		return checkgroup.UnknownMemberFunc
 	}
-
-	resultCh := make(chan checkgroup.Result)
-	go e.checkIsAllowed(ctx, r, restDepth)(ctx, resultCh)
-	select {
-	case result := <-resultCh:
-		return result
-	case <-ctx.Done():
-		return checkgroup.Result{Err: errors.WithStack(ctx.Err())}
+	return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
+		e.d.Logger().
+			WithField("request", r.String()).
+			Trace("check direct")
+		if rels, _, err := e.d.RelationTupleManager().GetRelationTuples(ctx, r.ToQuery()); err == nil && len(rels) > 0 {
+			resultCh <- checkgroup.Result{
+				Membership: checkgroup.IsMember,
+				Tree: &expand.Tree{
+					Type:  expand.Leaf,
+					Tuple: r,
+				},
+			}
+		} else {
+			resultCh <- checkgroup.Result{
+				Membership: checkgroup.NotMember,
+			}
+		}
 	}
 }
 
+// checkIsAllowed checks if the relation tuple is allowed (there is a path from
+// the relation tuple subject to the namespace, object and relation) either
+// directly (in the database), or through subject-set expansions, or through
+// user-set rewrites.
 func (e *Engine) checkIsAllowed(ctx context.Context, r *RelationTuple, restDepth int) checkgroup.CheckFunc {
 	if restDepth < 0 {
-		e.d.Logger().Debug("reached max-depth, therefore this query will not be further expanded")
+		e.d.Logger().
+			WithField("method", "checkIsAllowed").
+			Debug("reached max-depth, therefore this query will not be further expanded")
 		return checkgroup.UnknownMemberFunc
 	}
 
@@ -190,13 +183,8 @@ func (e *Engine) checkIsAllowed(ctx context.Context, r *RelationTuple, restDepth
 		Trace("check is allowed")
 
 	g := checkgroup.New(ctx)
-	g.Add(e.subQuery(r,
-		&Query{
-			Object:    r.Object,
-			Relation:  r.Relation,
-			Namespace: r.Namespace,
-		}, restDepth),
-	)
+	g.Add(e.checkDirect(ctx, r, restDepth-1))
+	g.Add(e.checkExpandSubject(ctx, r, restDepth))
 
 	relation, err := e.astRelationFor(ctx, r)
 	if err != nil {
diff --git a/internal/check/rewrites.go b/internal/check/rewrites.go
index d7bbd9cb0..af90308bd 100644
--- a/internal/check/rewrites.go
+++ b/internal/check/rewrites.go
@@ -157,6 +157,12 @@ func (e *Engine) checkInverted(
 	}
 }
 
+// checkComputedUserset rewrites the relation tuple to use the userset relation
+// instead of the the relation from the tuple.
+//
+// A relation tuple n:obj#original_rel@user is rewritten to
+// n:obj#userset@user, where the 'userset' relation is taken from the
+// userset.Relation.
 func (e *Engine) checkComputedUserset(
 	ctx context.Context,
 	r *RelationTuple,
@@ -185,8 +191,18 @@ func (e *Engine) checkComputedUserset(
 	)
 }
 
+// checkTupleToUserset rewrites the relation tuple to use the userset relation.
+//
+// Given a relation tuple like docs:readme#editor@user, and a tuple-to-userset
+// rewrite with the relation "parent" and the computed userset relation
+// "owner", the following checks will be performed:
+//
+// * query for all tuples like docs:readme#parent@??? to get a list of subjects
+//   that have the parent relation on docs:readme
+//
+// * For each matching subject, then check if subject#owner@user.
 func (e *Engine) checkTupleToUserset(
-	r *RelationTuple,
+	tuple *RelationTuple,
 	userset *ast.TupleToUserset,
 	restDepth int,
 ) checkgroup.CheckFunc {
@@ -196,7 +212,7 @@ func (e *Engine) checkTupleToUserset(
 	}
 
 	e.d.Logger().
-		WithField("request", r.String()).
+		WithField("request", tuple.String()).
 		WithField("tuple to userset relation", userset.Relation).
 		WithField("tuple to userset computed", userset.ComputedUsersetRelation).
 		Trace("check tuple to userset")
@@ -204,16 +220,16 @@ func (e *Engine) checkTupleToUserset(
 	return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
 		var (
 			prevPage, nextPage string
-			rts                []*RelationTuple
+			tuples             []*RelationTuple
 			err                error
 		)
 		g := checkgroup.New(ctx)
 		for nextPage = "x"; nextPage != "" && !g.Done(); prevPage = nextPage {
-			rts, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
+			tuples, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
 				ctx,
 				&Query{
-					Namespace: r.Namespace,
-					Object:    r.Object,
+					Namespace: tuple.Namespace,
+					Object:    tuple.Object,
 					Relation:  userset.Relation,
 				},
 				x.WithToken(prevPage))
@@ -222,17 +238,17 @@ func (e *Engine) checkTupleToUserset(
 				return
 			}
 
-			for _, rt := range rts {
-				if rt.Subject.SubjectSet() == nil {
+			for _, t := range tuples {
+				if t.Subject.SubjectSet() == nil {
 					continue
 				}
 				g.Add(e.checkIsAllowed(
 					ctx,
 					&RelationTuple{
-						Namespace: rt.Subject.SubjectSet().Namespace,
-						Object:    rt.Subject.SubjectSet().Object,
+						Namespace: t.Subject.SubjectSet().Namespace,
+						Object:    t.Subject.SubjectSet().Object,
 						Relation:  userset.ComputedUsersetRelation,
-						Subject:   r.Subject,
+						Subject:   tuple.Subject,
 					},
 					restDepth-1,
 				))