Skip to content

Commit 78d148e

Browse files
authored
Add Clerk Remix example (#186)
1 parent 89bd7a1 commit 78d148e

27 files changed

+12967
-0
lines changed

clerk-remix/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/.encore
2+
encore.gen.go
3+
encore.gen.cue

clerk-remix/README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Clerk Remix SDK + Encore App Example
2+
3+
This is an example of how to do user authentication using [Clerk](https://clerk.com/) together with an Encore app.
4+
Check out the [Use Clerk with your app](https://encore.dev/docs/go/how-to/clerk-auth) guide to learn more about this example.
5+
6+
## Cloning the example
7+
8+
When you have [installed Encore](https://encore.dev/docs/go/install), you can create a new Encore application and clone
9+
this example by running this command:
10+
11+
```bash
12+
encore app create my-app --example=clerk-remix
13+
```
14+
15+
## Clerk Credentials
16+
17+
Create a Clerk account if you haven't already. Then, in the Clerk dashboard, create a new application.
18+
19+
Next, go to the *API Keys* page for your app. Copy the "Publishable Key" and one of the "Secret keys".
20+
21+
In `frontend/.env` file, replace the values for `VITE_CLERK_PUBLISHABLE_KEY` with the value from your Clerk dashboard.
22+
23+
The `Secret key` is sensitive and should not be hardcoded in your code/config. Instead, you should store that as an [Encore secret](https://encore.dev/docs/go/primitives/secrets).
24+
25+
From your terminal (inside your Encore app directory), run:
26+
27+
```shell
28+
$ encore secret set --prod ClientSecretKey
29+
```
30+
31+
Next, do the same for the development secret. The most secure way is to create another secret key (Clerk allows you to have multiple).
32+
Once you have a client secret for development, set it similarly to before:
33+
34+
```shell
35+
$ encore secret set --dev ClientSecretKey
36+
```
37+
38+
## Developing locally
39+
40+
Run your Encore backend:
41+
42+
```bash
43+
encore run
44+
```
45+
46+
In a different terminal window, run the React frontend using [Remix](https://remix.run/):
47+
48+
```bash
49+
cd frontend
50+
npm install
51+
npm run dev
52+
```
53+
54+
Open [http://localhost:5173](http://localhost:5173) in your browser to see the result.
55+
56+
### Encore's Local Development Dashboard
57+
58+
While `encore run` is running, open [http://localhost:9400/](http://localhost:9400/) to view Encore's local developer dashboard.
59+
Here you can see the request you just made and a view a trace of the response.
60+
61+
### Generating a request client
62+
63+
Keep the contract between the backend and frontend in sync by regenerating the request client whenever you make a change
64+
to an Encore endpoint.
65+
66+
```bash
67+
npm run gen # Deployed Encore staging environment
68+
# or
69+
npm run gen:local # Locally running Encore backend
70+
```
71+
72+
## Deployment
73+
74+
### Encore
75+
76+
Deploy your backend to a staging environment in Encore's free development cloud:
77+
78+
```bash
79+
git add -A .
80+
git commit -m 'Commit message'
81+
git push encore
82+
```
83+
84+
Then head over to the [Cloud Dashboard](https://app.encore.dev) to monitor your deployment and find your production URL.
85+
86+
From there you can also see metrics, traces, connect your app to a
87+
GitHub repo to get automatic deploys on new commits, and connect your own AWS or GCP account to use for deployment.
88+
89+
### Remix on Vercel
90+
91+
1. Create a repo and push the project to GitHub.
92+
2. Create a new project on Vercel and point it to your GitHup repo.
93+
3. Select `frontend` as the root directory for the Vercel project.
94+
4. You may have some warning because the application uses @remix/node and not @remix/vercel
95+
96+
## CORS configuration
97+
98+
If you are running into CORS issues when calling your Encore API from your frontend then you may need to specify which
99+
origins are allowed to access your API (via browsers). You do this by specifying the `global_cors` key in the `encore.app`
100+
file, which has the following structure:
101+
102+
```js
103+
global_cors: {
104+
// allow_origins_without_credentials specifies the allowed origins for requests
105+
// that don't include credentials. If nil it defaults to allowing all domains
106+
// (equivalent to ["*"]).
107+
"allow_origins_without_credentials": [
108+
"<ORIGIN-GOES-HERE>"
109+
],
110+
111+
// allow_origins_with_credentials specifies the allowed origins for requests
112+
// that include credentials. If a request is made from an Origin in this list
113+
// Encore responds with Access-Control-Allow-Origin: <Origin>.
114+
//
115+
// The URLs in this list may include wildcards (e.g. "https://*.example.com"
116+
// or "https://*-myapp.example.com").
117+
"allow_origins_with_credentials": [
118+
"<DOMAIN-GOES-HERE>"
119+
]
120+
}
121+
```
122+
123+
More information on CORS configuration can be found here: https://encore.dev/docs/go/develop/cors

clerk-remix/backend/admin/admin.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package admin
2+
3+
import (
4+
"context"
5+
6+
a "encore.app/backend/auth"
7+
"encore.dev/beta/auth"
8+
"encore.dev/rlog"
9+
)
10+
11+
type DashboardData struct {
12+
Value string `json:"value"`
13+
}
14+
15+
// Endpoints annotated with `auth` are public and requires authentication
16+
// Learn more: encore.dev/docs/go/primitives/defining-apis#access-controls
17+
//
18+
//encore:api auth method=GET path=/admin
19+
func GetAdminDashboardData(ctx context.Context) (*DashboardData, error) {
20+
rlog.Info("Admin dashboard data requested", "user", auth.Data().(*a.UserData))
21+
return &DashboardData{Value: "Important stuff"}, nil
22+
}

clerk-remix/backend/auth/auth.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
6+
"encore.dev/beta/auth"
7+
"encore.dev/beta/errs"
8+
"github.com/clerk/clerk-sdk-go/v2/jwt"
9+
"github.com/clerk/clerk-sdk-go/v2/user"
10+
11+
"github.com/clerk/clerk-sdk-go/v2"
12+
)
13+
14+
var secrets struct {
15+
ClientSecretKey string
16+
}
17+
18+
// Service struct definition.
19+
// Learn more: encore.dev/docs/primitives/services-and-apis/service-structs
20+
//
21+
//encore:service
22+
type Service struct {
23+
}
24+
25+
// initService is automatically called by Encore when the service starts up.
26+
func initService() (*Service, error) {
27+
clerk.SetKey(secrets.ClientSecretKey)
28+
return &Service{}, nil
29+
}
30+
31+
type UserData struct {
32+
ID string `json:"id"`
33+
Username *string `json:"username"`
34+
FirstName *string `json:"first_name"`
35+
LastName *string `json:"last_name"`
36+
ProfileImageURL *string `json:"profile_image_url"`
37+
PrimaryEmailAddressID *string `json:"primary_email_address_id"`
38+
EmailAddresses []*clerk.EmailAddress `json:"email_addresses"`
39+
}
40+
41+
// The `encore:authhandler` annotation tells Encore to run this function for all
42+
// incoming API call that requires authentication.
43+
// Learn more: encore.dev/docs/go/develop/auth#the-auth-handler
44+
//
45+
//encore:authhandler
46+
func (s *Service) AuthHandler(ctx context.Context, token string) (auth.UID, *UserData, error) {
47+
// verify the session
48+
sessClaims, err := jwt.Verify(ctx, &jwt.VerifyParams{
49+
Token: token,
50+
})
51+
52+
if err != nil {
53+
return "", nil, &errs.Error{
54+
Code: errs.Unauthenticated,
55+
Message: "invalid token",
56+
}
57+
}
58+
59+
usr, err := user.Get(ctx, sessClaims.Subject)
60+
if err != nil {
61+
return "", nil, &errs.Error{
62+
Code: errs.Internal,
63+
Message: err.Error(),
64+
}
65+
}
66+
67+
userData := &UserData{
68+
ID: usr.ID,
69+
Username: usr.Username,
70+
FirstName: usr.FirstName,
71+
LastName: usr.LastName,
72+
ProfileImageURL: usr.ImageURL,
73+
PrimaryEmailAddressID: usr.PrimaryEmailAddressID,
74+
EmailAddresses: usr.EmailAddresses,
75+
}
76+
77+
return auth.UID(usr.ID), userData, nil
78+
}

clerk-remix/encore.app

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
// This is just an example so it's not linked to the Encore platform.
3+
"id": "",
4+
}

clerk-remix/frontend/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CLERK_PUBLISHABLE_KEY=pk_test_XX
2+
CLERK_SECRET_KEY=sk_test_XX
3+
4+
CLERK_SIGN_IN_URL=/sign-in
5+
CLERK_SIGN_UP_URL=/sign-up
6+
CLERK_SIGN_IN_FALLBACK_URL=/
7+
CLERK_SIGN_UP_FALLBACK_URL=/

clerk-remix/frontend/.eslintrc.cjs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* This is intended to be a basic starting point for linting in your app.
3+
* It relies on recommended configs out of the box for simplicity, but you can
4+
* and should modify this configuration to best suit your team's needs.
5+
*/
6+
7+
/** @type {import('eslint').Linter.Config} */
8+
module.exports = {
9+
root: true,
10+
parserOptions: {
11+
ecmaVersion: "latest",
12+
sourceType: "module",
13+
ecmaFeatures: {
14+
jsx: true,
15+
},
16+
},
17+
env: {
18+
browser: true,
19+
commonjs: true,
20+
es6: true,
21+
},
22+
ignorePatterns: ["!**/.server", "!**/.client"],
23+
24+
// Base config
25+
extends: ["eslint:recommended"],
26+
27+
overrides: [
28+
// React
29+
{
30+
files: ["**/*.{js,jsx,ts,tsx}"],
31+
plugins: ["react", "jsx-a11y"],
32+
extends: [
33+
"plugin:react/recommended",
34+
"plugin:react/jsx-runtime",
35+
"plugin:react-hooks/recommended",
36+
"plugin:jsx-a11y/recommended",
37+
],
38+
settings: {
39+
react: {
40+
version: "detect",
41+
},
42+
formComponents: ["Form"],
43+
linkComponents: [
44+
{ name: "Link", linkAttribute: "to" },
45+
{ name: "NavLink", linkAttribute: "to" },
46+
],
47+
"import/resolver": {
48+
typescript: {},
49+
},
50+
},
51+
},
52+
53+
// Typescript
54+
{
55+
files: ["**/*.{ts,tsx}"],
56+
plugins: ["@typescript-eslint", "import"],
57+
parser: "@typescript-eslint/parser",
58+
settings: {
59+
"import/internal-regex": "^~/",
60+
"import/resolver": {
61+
node: {
62+
extensions: [".ts", ".tsx"],
63+
},
64+
typescript: {
65+
alwaysTryTypes: true,
66+
},
67+
},
68+
},
69+
extends: [
70+
"plugin:@typescript-eslint/recommended",
71+
"plugin:import/recommended",
72+
"plugin:import/typescript",
73+
],
74+
},
75+
76+
// Node
77+
{
78+
files: [".eslintrc.cjs"],
79+
env: {
80+
node: true,
81+
},
82+
},
83+
],
84+
};

clerk-remix/frontend/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
3+
/.cache
4+
/build
5+
.env
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* By default, Remix will handle hydrating your app on the client for you.
3+
* You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4+
* For more information, see https://remix.run/file-conventions/entry.client
5+
*/
6+
7+
import { RemixBrowser } from "@remix-run/react";
8+
import { startTransition, StrictMode } from "react";
9+
import { hydrateRoot } from "react-dom/client";
10+
11+
startTransition(() => {
12+
hydrateRoot(
13+
document,
14+
<StrictMode>
15+
<RemixBrowser />
16+
</StrictMode>
17+
);
18+
});

0 commit comments

Comments
 (0)