diff --git a/Gopkg.lock b/Gopkg.lock
index 3c3e73496f..0a7579105d 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -186,6 +186,12 @@
   revision = "c74e4c2fa5c689d583d01521a7e6b2ecc1fddcde"
   version = "v0.9.14"
 
+[[projects]]
+  name = "github.com/ory/ladon"
+  packages = ["compiler"]
+  revision = "4223d97b7a16808bc1213cc641d529e764e67eea"
+  version = "v0.8.3"
+
 [[projects]]
   name = "github.com/pborman/uuid"
   packages = ["."]
@@ -339,6 +345,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "c857c9359eea153d02809743535bf58228f15d216ead4ed5babd07a77ed7297d"
+  inputs-digest = "11498cdf99d7b585598cf1667dc5c24db3c7e0d26a315cba59902a4876f2b5e5"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index 389fc4de73..819aa92106 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/golang/mock/gomock"
 	"github.com/ory/hydra/sdk/go/hydra"
 	"github.com/ory/hydra/sdk/go/hydra/swagger"
+	"github.com/ory/ladon/compiler"
 	"github.com/ory/oathkeeper/rule"
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
@@ -17,7 +18,7 @@ import (
 )
 
 func mustCompileRegex(t *testing.T, pattern string) *regexp.Regexp {
-	exp, err := regexp.Compile(pattern)
+	exp, err := compiler.CompileRegex(pattern, '<', '>')
 	require.NoError(t, err)
 	return exp
 }
@@ -30,15 +31,29 @@ func mustGenerateURL(t *testing.T, u string) *url.URL {
 
 func TestEvaluator(t *testing.T) {
 	we := NewWardenEvaluator(nil, nil, nil)
-	publicRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/[0-9]+"), AllowAnonymous: true}
-	bypassACPRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/[0-9]+"), BypassAccessControlPolicies: true}
-	privateRule := rule.Rule{
+	publicRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/<[0-9]+>"), AllowAnonymous: true}
+	bypassACPRule := rule.Rule{MatchesMethods: []string{"GET"}, MatchesPath: mustCompileRegex(t, "/users/<[0-9]+>"), BypassAccessControlPolicies: true}
+	privateRuleWithSubstitution := rule.Rule{
 		MatchesMethods:   []string{"POST"},
-		MatchesPath:      mustCompileRegex(t, "/users/([0-9]+)"),
+		MatchesPath:      mustCompileRegex(t, "/users/<[0-9]+>"),
 		RequiredResource: "users:$1",
 		RequiredAction:   "get:$1",
 		RequiredScopes:   []string{"users.create"},
 	}
+	privateRuleWithoutSubstitution := rule.Rule{
+		MatchesMethods:   []string{"POST"},
+		MatchesPath:      mustCompileRegex(t, "/users<$|/([0-9]+)>"),
+		RequiredResource: "users",
+		RequiredAction:   "get",
+		RequiredScopes:   []string{"users.create"},
+	}
+	privateRuleWithPartialSubstitution := rule.Rule{
+		MatchesMethods:   []string{"POST"},
+		MatchesPath:      mustCompileRegex(t, "/users<$|/([0-9]+)>"),
+		RequiredResource: "users:$2",
+		RequiredAction:   "get",
+		RequiredScopes:   []string{"users.create"},
+	}
 
 	for k, tc := range []struct {
 		d     string
@@ -215,7 +230,7 @@ func TestEvaluator(t *testing.T) {
 		},
 		{
 			d:     "request is denied because token is missing and endpoint is not public",
-			rules: []rule.Rule{privateRule},
+			rules: []rule.Rule{privateRuleWithSubstitution},
 			r:     &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
 			e: func(t *testing.T, s *Session, err error) {
 				require.Error(t, err)
@@ -226,7 +241,7 @@ func TestEvaluator(t *testing.T) {
 		},
 		{
 			d:     "request is denied because warden request fails with a network error and endpoint is not public",
-			rules: []rule.Rule{privateRule},
+			rules: []rule.Rule{privateRuleWithSubstitution},
 			r:     &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
 			e: func(t *testing.T, s *Session, err error) {
 				require.Error(t, err)
@@ -239,7 +254,7 @@ func TestEvaluator(t *testing.T) {
 		},
 		{
 			d:     "request is denied because warden request fails with a 400 status code and endpoint is not public",
-			rules: []rule.Rule{privateRule},
+			rules: []rule.Rule{privateRuleWithSubstitution},
 			r:     &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
 			e: func(t *testing.T, s *Session, err error) {
 				require.Error(t, err)
@@ -252,7 +267,7 @@ func TestEvaluator(t *testing.T) {
 		},
 		{
 			d:     "request is denied because warden request fails with allowed=false",
-			rules: []rule.Rule{privateRule},
+			rules: []rule.Rule{privateRuleWithSubstitution},
 			r:     &http.Request{Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
 			e: func(t *testing.T, s *Session, err error) {
 				require.Error(t, err)
@@ -264,8 +279,8 @@ func TestEvaluator(t *testing.T) {
 			},
 		},
 		{
-			d:     "request is allowed because token is valid and allowed",
-			rules: []rule.Rule{privateRule},
+			d:     "request is allowed because token is valid and allowed (rule with substitution)",
+			rules: []rule.Rule{privateRuleWithSubstitution},
 			r:     &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
 			e: func(t *testing.T, s *Session, err error) {
 				require.NoError(t, err)
@@ -282,6 +297,63 @@ func TestEvaluator(t *testing.T) {
 				return s
 			},
 		},
+		{
+			d:     "request is allowed because token is valid and allowed (rule with partial substitution)",
+			rules: []rule.Rule{privateRuleWithPartialSubstitution},
+			r:     &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
+			e: func(t *testing.T, s *Session, err error) {
+				require.NoError(t, err)
+			},
+			mock: func(c *gomock.Controller) hydra.SDK {
+				s := NewMockSDK(c)
+				s.EXPECT().DoesWardenAllowTokenAccessRequest(gomock.Eq(swagger.WardenTokenAccessRequest{
+					Token:    "token",
+					Resource: "users:1234",
+					Action:   "get",
+					Scopes:   []string{"users.create"},
+					Context:  map[string]interface{}{"remoteIpAddress": "127.0.0.1"},
+				})).Return(&swagger.WardenTokenAccessRequestResponse{Allowed: true}, &swagger.APIResponse{Response: &http.Response{StatusCode: http.StatusOK}}, nil)
+				return s
+			},
+		},
+		{
+			d:     "request is allowed because token is valid and allowed (rule with partial substitution and path parameter)",
+			rules: []rule.Rule{privateRuleWithoutSubstitution},
+			r:     &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users/1234")},
+			e: func(t *testing.T, s *Session, err error) {
+				require.NoError(t, err)
+			},
+			mock: func(c *gomock.Controller) hydra.SDK {
+				s := NewMockSDK(c)
+				s.EXPECT().DoesWardenAllowTokenAccessRequest(gomock.Eq(swagger.WardenTokenAccessRequest{
+					Token:    "token",
+					Resource: "users",
+					Action:   "get",
+					Scopes:   []string{"users.create"},
+					Context:  map[string]interface{}{"remoteIpAddress": "127.0.0.1"},
+				})).Return(&swagger.WardenTokenAccessRequestResponse{Allowed: true}, &swagger.APIResponse{Response: &http.Response{StatusCode: http.StatusOK}}, nil)
+				return s
+			},
+		},
+		{
+			d:     "request is allowed because token is valid and allowed (rule without substitution and path parameter)",
+			rules: []rule.Rule{privateRuleWithoutSubstitution},
+			r:     &http.Request{RemoteAddr: "127.0.0.1:1234", Method: "POST", Header: http.Header{"Authorization": []string{"bEaReR token"}}, URL: mustGenerateURL(t, "https://localhost/users")},
+			e: func(t *testing.T, s *Session, err error) {
+				require.NoError(t, err)
+			},
+			mock: func(c *gomock.Controller) hydra.SDK {
+				s := NewMockSDK(c)
+				s.EXPECT().DoesWardenAllowTokenAccessRequest(gomock.Eq(swagger.WardenTokenAccessRequest{
+					Token:    "token",
+					Resource: "users",
+					Action:   "get",
+					Scopes:   []string{"users.create"},
+					Context:  map[string]interface{}{"remoteIpAddress": "127.0.0.1"},
+				})).Return(&swagger.WardenTokenAccessRequestResponse{Allowed: true}, &swagger.APIResponse{Response: &http.Response{StatusCode: http.StatusOK}}, nil)
+				return s
+			},
+		},
 	} {
 		t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) {
 			ctrl := gomock.NewController(t)
@@ -295,3 +367,14 @@ func TestEvaluator(t *testing.T) {
 		})
 	}
 }
+
+func TestSubstitution(t *testing.T) {
+	reg, err := compiler.CompileRegex("/rules<$|/([^/]+)>", '<', '>')
+	fmt.Println(reg.String())
+	fmt.Printf("Found: %s\n", reg.FindAllString("/rules", -1))
+	fmt.Printf("Found: %s\n", reg.FindAllString("/rules/", -1))
+	fmt.Printf("Found: %s\n", reg.FindAllString("/rules/2423", -1))
+	fmt.Printf("Found: %s\n", reg.ReplaceAllString("/rules/2423", "read:$2"))
+	require.NoError(t, err)
+
+}
diff --git a/rule/handler.go b/rule/handler.go
index ff1410036b..85d51ebcc8 100644
--- a/rule/handler.go
+++ b/rule/handler.go
@@ -3,10 +3,10 @@ package rule
 import (
 	"encoding/json"
 	"net/http"
-	"regexp"
 
 	"github.com/julienschmidt/httprouter"
 	"github.com/ory/herodot"
+	"github.com/ory/ladon/compiler"
 	"github.com/ory/oathkeeper/helper"
 	"github.com/pborman/uuid"
 	"github.com/pkg/errors"
@@ -198,7 +198,7 @@ func decodeRule(w http.ResponseWriter, r *http.Request) (*Rule, error) {
 }
 
 func toRule(rule *jsonRule) (*Rule, error) {
-	exp, err := regexp.Compile(rule.MatchesPath)
+	exp, err := compiler.CompileRegex(rule.MatchesPath, '<', '>')
 	if err != nil {
 		return nil, err
 	}
diff --git a/rule/manager_sql.go b/rule/manager_sql.go
index 9570c554b4..abbdede008 100644
--- a/rule/manager_sql.go
+++ b/rule/manager_sql.go
@@ -3,11 +3,11 @@ package rule
 import (
 	"database/sql"
 	"fmt"
-	"regexp"
 	"strings"
 
 	"github.com/jmoiron/sqlx"
 	_ "github.com/lib/pq"
+	"github.com/ory/ladon/compiler"
 	"github.com/ory/oathkeeper/helper"
 	"github.com/pkg/errors"
 	"github.com/rubenv/sql-migrate"
@@ -27,7 +27,7 @@ type sqlRule struct {
 }
 
 func (r *sqlRule) toRule() (*Rule, error) {
-	exp, err := regexp.Compile(r.MatchesPath)
+	exp, err := compiler.CompileRegex(r.MatchesPath, '<', '>')
 	if err != nil {
 		return nil, errors.WithStack(err)
 	}
diff --git a/rule/manager_test.go b/rule/manager_test.go
index 25a9d95a73..d5e77dc16e 100644
--- a/rule/manager_test.go
+++ b/rule/manager_test.go
@@ -9,6 +9,7 @@ import (
 
 	"github.com/jmoiron/sqlx"
 	"github.com/ory/dockertest"
+	"github.com/ory/ladon/compiler"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -26,7 +27,7 @@ func kjillAll() {
 }
 
 func mustCompileRegex(t *testing.T, pattern string) *regexp.Regexp {
-	exp, err := regexp.Compile(pattern)
+	exp, err := compiler.CompileRegex(pattern, '<', '>')
 	require.NoError(t, err)
 	return exp
 }
diff --git a/rule/matcher_test.go b/rule/matcher_test.go
index df4c1e1165..5d0a1f548d 100644
--- a/rule/matcher_test.go
+++ b/rule/matcher_test.go
@@ -3,9 +3,10 @@ package rule
 import (
 	"fmt"
 	"net/url"
-	"regexp"
 	"strconv"
 	"testing"
+
+	"github.com/ory/ladon/compiler"
 )
 
 var methods = []string{"POST", "PUT", "GET", "DELETE", "PATCH", "OPTIONS", "HEAD"}
@@ -13,12 +14,12 @@ var methods = []string{"POST", "PUT", "GET", "DELETE", "PATCH", "OPTIONS", "HEAD
 func generateDummyRules(amount int) []Rule {
 	rules := make([]Rule, amount)
 	scopes := []string{"foo", "bar", "baz", "faz"}
-	expressions := []string{"/users/", "/users", "/blogs/", "/use(r)s/"}
+	expressions := []string{"/users/", "/users", "/blogs/", "/use<(r)>s/"}
 	resources := []string{"users", "users:$1"}
 	actions := []string{"get", "get:$1"}
 
 	for i := 0; i < amount; i++ {
-		exp, _ := regexp.Compile(expressions[(i%(len(expressions)))] + "([0-" + strconv.Itoa(i) + "]+)")
+		exp, _ := compiler.CompileRegex(expressions[(i%(len(expressions)))]+"([0-"+strconv.Itoa(i)+"]+)", '<', '>')
 		rules[i] = Rule{
 			ID:               strconv.Itoa(i),
 			MatchesMethods:   methods[:i%(len(methods))],