-
Notifications
You must be signed in to change notification settings - Fork 75
Encore.go SaaS Starter Template #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MaxHolmberg96
wants to merge
2
commits into
encoredev:main
Choose a base branch
from
MaxHolmberg96:encore-saas-template-go
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/.encore | ||
encore.gen.go | ||
encore.gen.cue | ||
/encore.gen | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
|
||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
|
||
# Directory for instrumented libs generated by jscoverage/JSCover | ||
lib-cov | ||
|
||
# Coverage directory used by tools like istanbul | ||
coverage | ||
|
||
# nyc test coverage | ||
.nyc_output | ||
|
||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | ||
.grunt | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# Compiled binary addons (http://nodejs.org/api/addons.html) | ||
build/Release | ||
|
||
# Dependency directories | ||
node_modules | ||
jspm_packages | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional REPL history | ||
.node_repl_history | ||
.next | ||
|
||
.cursorignore | ||
.cursorrules |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
# EncoreKit: Encore SaaS Template | ||
|
||
## Features | ||
|
||
- Landing page with feature promotion | ||
- Pricing page (/pricing) which connects to Stripe Checkout | ||
- Dashboard pages with CRUD operations to modify user | ||
- Admin role in firebase claims (admins sees activity) | ||
- Subscription management with Stripe Customer Portal | ||
- Authentication with firebase | ||
- Activity logging system for any user events | ||
|
||
## Tech stack | ||
|
||
- Frontend Framework: [Next.js](https://nextjs.org/) | ||
- Backend Framenwork [encore.go](https://encore.dev/go?utm_source=google&utm_medium=cpc&utm_campaign=21986726527&utm_term=encore%20go&gad_source=1&gclid=Cj0KCQiAoJC-BhCSARIsAPhdfSiHgVYvaxgmhP7cB6mdt7RaAkeRno5xU0F9stTWj9_tJBEy4lYauqIaAt0hEALw_wcB) with postgres database | ||
- ORM: [gorm go](https://gorm.io/) | ||
- Payments: [Stripe](https://stripe.com/) | ||
- UI Library: [shadcn/ui](https://ui.shadcn.com/) | ||
|
||
## Developing locally | ||
|
||
When you have [installed Encore](https://encore.dev/docs/install), you can create a new Encore application and clone this example with this command. | ||
|
||
```bash | ||
encore app create my-app-name --example=encore-saas-template | ||
``` | ||
|
||
## Running locally | ||
|
||
### Backend | ||
|
||
Running the backend requires the following scripts: | ||
|
||
```bash | ||
encore run # This has to be run to setup the postgres docker db and volume for later steps | ||
``` | ||
|
||
```bash | ||
pnpm i | ||
``` | ||
|
||
Seeding the users in Firebase and Postgres. Find service-account.json in Firebase Console > Project Setting > Service Accounts > **Generate new private key**. (note: run script setup-firebase-and-users:clean to remove users) | ||
|
||
```bash | ||
pnpm setup-firebase-and-users --service-account "/path/to/service-account.json" | ||
``` | ||
#### Listen to the webhook to get subscription events | ||
Set Stripe secrets and setup a stripe webhook (requires [Stripe CLI](https://docs.stripe.com/stripe-cli) is installed) | ||
|
||
This command also runs stripe listen to forward stripe webhooks to our backend locally. | ||
|
||
```bash | ||
pnpm run setup-stripe | ||
``` | ||
|
||
### Frontend | ||
|
||
```bash | ||
cd frontend | ||
pnpm i | ||
pnpm gen:local # generate a client for the frontend to encores cli with | ||
pnpm dev | ||
``` | ||
|
||
## Testing Payments | ||
|
||
To test Stripe payments, use the following test card details: | ||
|
||
Card Number: 4242 4242 4242 4242 | ||
Expiration: Any future date | ||
CVC: Any 3-digit number | ||
|
||
## Local Development Dashboard | ||
|
||
While `encore run` is running, open <http://localhost:9400/> to access Encore's [local developer dashboard](https://encore.dev/docs/observability/dev-dash). | ||
|
||
Here you can see API docs, make requests in the API explorer, and view traces of the responses. | ||
|
||
## Deploying to Encore | ||
|
||
Deploy your application to a staging environment in Encore's free development cloud: | ||
|
||
```bash | ||
git add . | ||
git commit -m "first commit" | ||
git push encore | ||
``` | ||
|
||
### Allow Vercel domain to access Encore | ||
|
||
Modify encore.app to look like: | ||
|
||
``` | ||
{ | ||
"id": "<encore-app-id>", | ||
"global_cors": { | ||
"allow_origins_with_credentials": [ | ||
"http://127.0.0.1:3000", | ||
"http://localhost:3000", | ||
"https://<vercel-app-name>.vercel.app" | ||
] | ||
} | ||
} | ||
``` | ||
|
||
Then head over to the [Cloud Dashboard](https://app.encore.dev) to monitor your deployment and find your production URL. | ||
|
||
From there you can also connect your own AWS or GCP account to use for deployment. | ||
|
||
## Deploying to Vercel | ||
|
||
1. Push your code to a GitHub repository. | ||
2. Connect your repository to Vercel and deploy it. | ||
3. Follow the Vercel deployment process, which will guide you through setting up your project. | ||
4. **Remember to setup the env variables in Vercel** | ||
|
||
### Allow Vercel domain to access Firebase authentication | ||
|
||
1. Go to [Firebase Console](https://console.firebase.google.com/) | ||
2. Go to Authentication -> Settings | ||
3. Add the vercel domain to authorized domains. | ||
|
||
# Getting ready for production | ||
|
||
## Stripe | ||
|
||
### Set up a production Stripe webhook | ||
|
||
1. Go to the Stripe Dashboard and create a new webhook for your production environment. | ||
2. Set the endpoint URL to your production API route (e.g., `https://yourdomain.com/api/stripe/webhook`). | ||
3. Select the events you want to listen for (e.g., `checkout.session.completed`, `customer.subscription.updated`). | ||
|
||
### Configure Stripe secrets | ||
|
||
```bash | ||
encore secret set --type production StripeSecretKey | ||
encore secret set --type production StripeWebhookSecret | ||
encore secret set --type production CallbackURL | ||
``` | ||
|
||
## Encore | ||
|
||
Setup a production environment in [Encore's cloud dashboard](https://app.encore.cloud) and link to the branch of choice. | ||
Then on the production branch: | ||
|
||
```bash | ||
git commit encore | ||
``` | ||
|
||
### Secrets required in Encore | ||
|
||
```bash | ||
encore secret set --type production FirebasePrivateKey < "/path/to/service-account.json" | ||
``` | ||
|
||
## Vercel | ||
|
||
Remember to setup the environment variables required in Vercel. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package activity | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"encore.app/backend/user" | ||
"encore.dev/beta/errs" | ||
"encore.dev/rlog" | ||
"github.com/google/uuid" | ||
) | ||
|
||
type ActivityResponse struct { | ||
ID string `json:"id"` | ||
UserID string `json:"user_id"` | ||
Event string `json:"event"` | ||
CreatedAt time.Time `json:"created_at"` | ||
} | ||
|
||
type ActivitiesResponse struct { | ||
Activities []*ActivityResponse `json:"activities"` | ||
} | ||
|
||
type FilterActivitiesRequest struct { | ||
Offset int `json:"offset"` | ||
Limit int `json:"limit"` | ||
} | ||
|
||
type CreateActivityRequest struct { | ||
UserID string `json:"user_id"` | ||
Event string `json:"event"` | ||
} | ||
|
||
//encore:api auth method=GET path=/v1/activities tag:admin | ||
func (s *Service) GetActivities(ctx context.Context, p *FilterActivitiesRequest) (*ActivitiesResponse, error) { | ||
eb := errs.B() | ||
|
||
offset := p.Offset | ||
limit := p.Limit | ||
|
||
if offset < 0 { | ||
eb = eb.Code(errs.InvalidArgument).Msg("offset must be greater than 0") | ||
} | ||
|
||
if limit < 0 { | ||
eb = eb.Code(errs.InvalidArgument).Msg("limit must be greater than 0") | ||
} | ||
|
||
activities := make([]*Activity, 0) | ||
err := s.db.Find(&Activity{}).Offset(offset).Limit(limit).Find(&activities).Error | ||
if err != nil { | ||
return nil, eb.Cause(err).Code(errs.Internal).Msg("failed to get activities").Err() | ||
} | ||
|
||
activitiesResponse := make([]*ActivityResponse, 0) | ||
for _, activity := range activities { | ||
activitiesResponse = append(activitiesResponse, &ActivityResponse{ | ||
ID: activity.ID, | ||
UserID: activity.UserID, | ||
Event: activity.Event, | ||
CreatedAt: activity.CreatedAt, | ||
}) | ||
} | ||
|
||
return &ActivitiesResponse{ | ||
Activities: activitiesResponse, | ||
}, nil | ||
} | ||
|
||
func (s *Service) HandleSignupEvents(ctx context.Context, p *user.SignupEvent) error { | ||
eb := errs.B() | ||
rlog.Info("signup event", "user_id", p.UserID) | ||
|
||
activity := Activity{ | ||
ID: uuid.NewString(), | ||
UserID: p.UserID, | ||
Event: "signup", | ||
CreatedAt: time.Now(), | ||
} | ||
|
||
if err := s.db.Create(&activity).Error; err != nil { | ||
return eb.Cause(err).Code(errs.Internal).Msg("failed to create activity").Err() | ||
} | ||
|
||
return nil | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package activity | ||
|
||
import ( | ||
"errors" | ||
|
||
a "encore.app/backend/auth" | ||
"encore.dev/beta/auth" | ||
"encore.dev/beta/errs" | ||
"encore.dev/middleware" | ||
) | ||
|
||
// ValidateAdmin validates the roles of the user. | ||
// | ||
//encore:middleware target=tag:admin | ||
func ValidateAdmin(req middleware.Request, next middleware.Next) middleware.Response { | ||
userData := auth.Data().(*a.UserData) | ||
|
||
if userData.Role != "admin" { | ||
err := errs.WrapCode(errors.New("permission denied"), errs.PermissionDenied, "user is not an admin") | ||
return middleware.Response{Err: err} | ||
} | ||
|
||
return next(req) | ||
} |
6 changes: 6 additions & 0 deletions
6
encore-saas-template/backend/activity/migrations/0001_create_table_activities.up.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
CREATE TABLE activities ( | ||
id TEXT PRIMARY KEY, | ||
user_id VARCHAR(255) NOT NULL, | ||
event VARCHAR(255) NOT NULL, | ||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP | ||
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package activity | ||
|
||
import ( | ||
"time" | ||
|
||
"encore.app/backend/user" | ||
"encore.dev/pubsub" | ||
"encore.dev/storage/sqldb" | ||
"gorm.io/driver/postgres" | ||
"gorm.io/gorm" | ||
) | ||
|
||
type Activity struct { | ||
ID string `gorm:"primaryKey;type:text" json:"id"` | ||
UserID string `gorm:"not null;type:text" json:"user_id"` | ||
Event string `gorm:"not null;type:text" json:"event"` | ||
CreatedAt time.Time `gorm:"not null;type:timestamp" json:"created_at"` | ||
} | ||
|
||
//encore:service | ||
type Service struct { | ||
db *gorm.DB | ||
} | ||
|
||
var db = sqldb.NewDatabase("activities", sqldb.DatabaseConfig{ | ||
Migrations: "./migrations", | ||
}) | ||
|
||
func initService() (*Service, error) { | ||
db, err := gorm.Open(postgres.New(postgres.Config{ | ||
Conn: db.Stdlib(), | ||
})) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &Service{db: db}, nil | ||
} | ||
|
||
var _ = pubsub.NewSubscription( | ||
user.Signups, "signups", | ||
pubsub.SubscriptionConfig[*user.SignupEvent]{ | ||
Handler: pubsub.MethodHandler((*Service).HandleSignupEvents), | ||
}, | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.