-
Notifications
You must be signed in to change notification settings - Fork 103
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reader library directory in text content; along with linking palette for control and function on and off the usb utility sourcing...
This comment was marked as resolved.
Sorry, something went wrong. |
||
| build/Release | ||
|
|
||
| # Dependency directories | ||
| node_modules | ||
| jspm_packages | ||
|
|
||
| # Optional npm cache directory | ||
| .npm | ||
|
|
||
| # Optional REPL history | ||
| .node_repl_history | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. X = 0 \ if x != 3: \ print "x does not equal 3" \ \ \ class Program \ { \ static void Main(string)[] args) \ \ True \ \ \ Act \ <xsd:element name="Active"type="XS:boolean"/> \ Valid Value 73 val. TRUE \ \ true \ \ XML Schema specification \ \ 3.2.2.1 Lexical representation an instance of a datatype that is defined as -boolean- can have the following legal literals {true, false, |,0}. <xs:simpletype name="my:boolean"> 2023.5.18.43442 |
||
| .next | ||
|
|
||
| .cursorignore | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. enable curser, screen keyboard, mouse icon |
||
| .cursorrules | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| # 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 Framework [Encore.go](https://encore.dev/go) | ||
|
|
||
| - 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) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stricked Huffman Node {
This comment was marked as resolved.
Sorry, something went wrong. |
||
|
|
||
| This command also runs stripe listen to forward stripe webhooks to our backend locally. | ||
|
|
||
| ```bash | ||
| pnpm run setup-stripe | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IP:2603:6011:6001:7782:697d:ce3e:862:6712 <script> //2. This code loads the IFrame Player API code asynchronously. var tag = document.createElement(`script`); |
||
| ``` | ||
|
|
||
| ### 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Card Number:[5][2][8][8][4][7][0][0][2][6][6][8][3][2][4][0] |
||
| Expiration: Any future date | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Experation:[01]/[27] |
||
| CVC: Any 3-digit number | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CVC:[0][4][7] |
||
|
|
||
| ## 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. | ||
| 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 | ||
| } |
| 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) | ||
| } |
| 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 | ||
| ); |
| 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), | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<import=? Path: DPInst (c:\Program files (X86) \ Realtek \ PCIE \
wireless \ LAN \ RTWLANE_Driver \ DPInst \ X86