Serve your site and sell courses with self hosted server.
sserver is simple headless server for hosting courses and associated blog/static content from private github repository with minimal overhead.
It provides https out of the box so you don't have to deal with installing/managing certificates. It syncs the content automatically from github so you don't have to upload your content to server. It also supports premium content with simple configuration file without affecting your content workflow for e.g. if you are using static site generator like hugo you can hide the premium content from public by specifying it in config file. It has stripe integration so you can sell your premium content. It has user management built in for adding/authenticating users. It also has admin api so you can monitor orders/users of your site.
- Serve static content from github/local directory
- Auto https support via letsencrypt
- Auto sync content from github repository
- Support for gated content/course
- Stripe integration for selling course
- User registration and authentication
- Admin apis to get overall view of the store
Here are some examples of how it can be used, please make sure to set SS_<> environment variables before trying them out e.g.
export SS_GITHUB_TOKEN=<github_token>
export SS_STRIPE_TOKEN=<stripe_token>
export SS_SMTP_FROM=<email_id>
export SS_SMTP_USER=<smtp_username>
export SS_SMTP_PWD=<smtp_password>
export SS_SMTP_HOST=<smtp_host_address>
export SS_SMTP_PORT=<smtp_port>
export SS_ADMIN_EMAIL=<admin_email>
export SS_ADMIN_PWD=<admin_password_for_sserver>
./sserver -repo "https://github.com/newbeelearn/sserver.git"
Repository should have index.html and ssconfig.toml in its root directory. If hugo/jekyll etc. like static site generator is used, repository should contain generated site.(It would have index.html in root by default)
./sserver -repo "https://github.com/newbeelearn/sserver.git?folder=public"
Repository should have index.html in folder from where you want to serve the content, typically it is public if hugo/jekyll etc. static site generators are used. It should have ssconfig.toml in the root directory
./sserver -repo "https://github.com/newbeelearn/sserver.git?ref=test-config"
Branch should have index.html and ssconfig.toml in its root directory. If hugo/jekyll etc. like static site generator is used, branch should contain generated site.(It would have index.html in root by default)
./sserver -repo "https://github.com/newbeelearn/sserver.git?domain=example.com"
Repository should have index.html and ssconfig.toml in its root directory Access to the domain from which site is served sserver should have permissions to bind to 443 port, this can be done with following command
sudo setcap 'cap_net_bind_service=+ep' sserver
./sserver -repo "file:///workspace/projects/newbeelearn.com/sserver"
Repository should have index.html and ssconfig.toml in its root directory. All the options i.e. folder/domain etc. can be specified in case of local files as well
Sample ssconfig.toml can be found below
# specify the site
[site]
# period to check for new content
syncinterval = "@every 12h"
# product/course details
[[site.prod]]
name = "course1"
# path from root, this will be accessible to users who have bought the course
path = "courses/course1"
# can be draft/active, buying functionality will be enabled when status is active
status = "active"
# unique identifier for the course
sku = "prod-course-1"
# price in cents
price = 10000
# currency
currency = "USD"
sserver creates "wwwss" directory from where it is run
drwxrwxr-x 2 test test 4096 Nov 30 17:04 a
drwxrwxr-x 8 test test 4096 Nov 30 17:04 b
drwxrwxr-x 2 test test 4096 Nov 30 17:04 certs
drwxrwxr-x 2 test test 4096 Nov 30 17:04 logs
-rw-rw-r-- 1 test test 527483 Nov 30 17:04 tmp.zip
-rw-rw-r-- 1 test test 49152 Nov 30 17:05 ssapp.db
- If https is used key and certificates can be found in certs
- Server access logs are inside logs folder
- Database of users/permissions and course details are in sqlite file ssapp.db
- tmp.zip is temporary site downloaded from github in zip format and is overwritten on every sync
- a/b folders is from where site is served. Actual folder keeps on alternating between the two.
Configuration file is used to specify the products/courses that you want to sell as well as some server parameters like how often the site should be synced etc.
- syncinterval specifies auto syncing period for content default is 12 hours
- login specifies login/sign-in page to be redirected to if user tries to access protected content if not specified it redirects to home page of the site
- postlogin specifies page user should be redirected to in case of successful login if not specified it returns json response which needs to be processed from client side
- postsignup specifies page user should be redirected to after completing the signup if not specified it returns json response which needs to be interpreted on client side
- name is the name of the product course
- path is the path of course content relative to the root directory. This will not be accessible without registering and buying the course.
- status can be either draft or active. Buying functionality becomes available only when product status is active
- sku this should be unique identifier for the course/product. It creates stripe product with the identifier specified in sku. If you already have a product in stripe use its id as sku otherwise duplicate products will be created.
- price is in cents for USD, i.e. 10.58USD course should set the price as 1058. For other currencies please check stripe documentation as the software currently assumes stripe convention for currency and price is followed.
- currency should follow stripe convention
Sample ssconfig.toml file is shown below
#specify the site
[site]
#period to check for new content default is 12 hours
syncinterval = "@every 12h"
[[site.prod]]
name = "course1"
path = "courses/course1"
status = "active"
sku = "prod-course-1"
price = 10000
currency = "USD"
- If domain is not specified content will be served from port 54545 else port 443
- If local file is specified content will be served from that directory else it would be served from wwwss folder which will autosync content from github repository
- If syncinterval is specified it will sync content from github/check content from local file with syncinterval duration else it will sync/check for new content every 12 hours
- Syncing between stripe events i.e. refund done through stripe site etc. are synced every 6 hours
- If SMTP details are not specified, mail sending functionality on registration/reset etc. will be disabled
All api endpoints are relative to the domain used for serving the content
i.e. if domain is example.com and api is /api/v1/product/list
request would be https://example.com/api/v1/product/list
Role hierarchy is like this admin > user > guest Any api accessible to guest is also accessible to user and any api accessible to user is also accessible to admin For accessing user/admin api's session cookie obtained after logging in must be supplied with each request in practice this will be taken care by the browser
Description | Request | Role |
---|---|---|
Register User | POST /api/v1/user/register |
guest |
Login User | POST /api/v1/user/login |
guest |
Logout user | GET /api/v1/user/logout |
guest |
Verify user | GET /api/v1/user/verify/:id |
guest |
Reset user password | POST /api/v1/user/reset |
guest |
Get product list | GET /api/v1/product/list |
guest |
Create order | POST /api/v1/order/id |
guest |
Modify order | PUT /api/v1/order/id |
user |
Checkout order | POST /api/v1/order/checkout |
user |
Get order by order id | GET /api/v1/order/id/:id |
user |
Get all orders by user | GET /api/v1/order/id/list |
user |
Change user password | POST /api/v1/user/changepwd |
user |
Get all orders | GET /api/v1/order/list |
admin |
Get all users | GET /api/v1/user/list |
admin |
Register new user with email and password. Sends mail to the email used to register with verification code if SMTP server configuration is set.
-
Example Request
curl 'http://localhost:54545/api/v1/user/register' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -X POST \ --data-raw 'email=stripe%40newbeelearn.com&password=test&confirm-password=test&remember=on'
-
Example Response
{"status":"success"}
Login user with email and password. Returns json if "postlogin" field is not set in config file otherwise redirects to the page specified in "postlogin"
-
Example Request
curl 'http://localhost:54545/api/v1/user/login' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -X POST \ --data-raw 'email=admin%40example.com&password=admin'
-
Example Response
{ "data": { "user_id": "1", "username": "" }, "msg": "user found", "status": "success" }
Logs out logged in user and redirects to homepage
-
Example Request
curl 'http://localhost:54545/api/v1/user/logout' \ -H 'Cookie: session_id=9e8b22a3-15ac-442f-bf65-15c37dbfc889; max-age=300; path=/; secure; SameSite=Lax'
-
Example Response
<!doctype html> <html lang="en"> <head> </head> <body> </body> </html>
Verifies the email of registered user by sending url if domain is set or code that should be appended after the verify api if domain is not set
-
Example Request
curl 'http://localhost:54545/api/v1/user/verify/cafj5grn0gpog1j3a0m0'
-
Example Response
{"status":"success"}
Resets the user password and sends the new temporary code for login. User needs to use this code on next login and change the password
-
Example Request
curl 'http://localhost:54545/api/v1/user/reset' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -X POST \ --data-raw 'email=stripe%40newbeelearn.com'
-
Example Response
{ "data": null, "msg": "password reset successful", "status": "success" }
Get all the products listed for sale on website. Takes "limit" and "offset" as queries. If query is not set default values of limit is set to 10 and offset to 0
-
Example Request
curl 'http://localhost:54545/api/v1/product/list?limit=1&offset=0'
-
Example Response
{ "data": [ { "prd_id": 1, "prd_name": "course1", "sku": "prod-course-1", "permalink": "users/list/", "price": 10000, "currency": "USD", "period": 365, "status": "active" } ], "msg": "Order found", "status": "success" }
Creates new order with products listed in "line_item"
field. Request should be valid json.
Actual order_id/user_id
etc. fields are populated by the server any dummy value can be passed.
-
Example Request
curl 'http://localhost:54545/api/v1/order/id' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=cad8439e-dcc4-475e-94fc-12b75f85bb20; max-age=300; path=/; secure; SameSite=Lax' \ -X POST \ --data-raw ' { "order_id": 1, "user_id": 3, "currency": "USD", "line_items": [ { "sku": "prod-course-1" }, { "sku": "prod-course-3" } ] }'
-
Example Response
{ "data": { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "grand_total": 11000, "line_items": [ { "line_id": 1, "order_id": 1, "prd_id": 1, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "grand": 10000, "enabled": true, "sku": "prod-course-1" }, { "line_id": 2, "order_id": 1, "prd_id": 2, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "grand": 1000, "enabled": true, "sku": "prod-course-3" } ] }, "msg": "Order found", "status": "success" }
Modifies existing order by adding/deleting products in line_item
field. User must be logged in to
modify the order and order should be in active state. Use the previous response of create order or get order
to add/delete products
-
Example Request
curl 'http://localhost:54545/api/v1/order/id' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=cad8439e-dcc4-475e-94fc-12b75f85bb20; max-age=300; path=/; secure; SameSite=Lax' \ -X PUT \ --data-raw ' { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "grand_total": 11000, "line_items": [ { "line_id": 2, "order_id": 1, "prd_id": 2, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "grand": 1000, "enabled": true, "sku": "prod-course-3" } ] }'
-
Example Response
{ "data": { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:48:05.425488765 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "grand_total": 1000, "line_items": [ { "line_id": 2, "order_id": 1, "prd_id": 2, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:48:05.425488765 +0000 UTC", "grand": 1000, "enabled": true, "sku": "prod-course-3" } ] }, "msg": "Order found", "status": "success" }
Gets stripe url for payment of order created by create order api. Use the response from create order/modify order/get order api to send the request. Do not modify the response in this request it will result in failure.
-
Example Request
curl 'http://localhost:54545/api/v1/order/checkout' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=2f1be070-7256-4e84-a4ef-c14754cabcdb; max-age=300; path=/; secure; SameSite=Lax' \ -X POST \ --data-raw ' { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "grand_total": 11000, "line_items": [ { "line_id": 2, "order_id": 1, "prd_id": 2, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "grand": 1000, "enabled": true, "sku": "prod-course-3" } ] }'
-
Example Response
{ "data": { "url": "https://checkout.stripe.com/pay/cs_test_a17D2l74NsKMv29YJ1c5rSBPx7BGSsNAsObGAsOanEJqyFNXKEYDLji4BZ#fidkdWxOYHwnPyd1blpxYHZxWjA0TlVKPHNMaW9vYEd1YmhdUWQ3UUJqSEpMYTMza11ObGAyXDFPcXA8bz1yY1VicVZVdDN8c1NkaUZEazxIQWdjM04wdz1DTmF3PXxHaVE9bTVuZz1pUWw3NTUybHZLZldgaicpJ2N3amhWYHdzYHcnP3F3cGApJ2lkfGpwcVF8dWAnPyd2bGtiaWBabHFgaCcpJ2BrZGdpYFVpZGZgbWppYWB3dic%2FcXdwYHgl" }, "msg": "Order found", "status": "success" }
Get order details by order number
-
Example Request
curl 'http://localhost:54545/api/v1/order/id/cafjktbn0gpp5hq3dt4g' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=fe9b9fff-c5c0-4745-becf-ecb0e5abca81; max-age=300; path=/; secure; SameSite=Lax'
-
Example Response
{ "data": { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:48:05.425488765 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "grand_total": 1000, "line_items": [ { "line_id": 2, "order_id": 1, "prd_id": 2, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:48:05.425488765 +0000 UTC", "grand": 1000, "enabled": true } ] }, "msg": "Order found", "status": "success" }
Get all orders by user. Takes limit and offset as query parameters default values are 10 and 0 respectively
-
Example Request
curl 'http://localhost:54545/api/v1/order/id/list?limit=1&offset=0' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=fe9b9fff-c5c0-4745-becf-ecb0e5abca81; max-age=300; path=/; secure; SameSite=Lax'
-
Example Response
{ "data": [ { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:48:05.425488765 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "grand_total": 1000 } ], "msg": "Order found", "status": "success" }
Change user password. User must be logged in to make this request
-
Example Request
curl 'http://localhost:54545/api/v1/user/changepwd' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Cookie: session_id=fe9b9fff-c5c0-4745-becf-ecb0e5abca81; max-age=300; path=/; secure; SameSite=Lax' \ -X POST \ --data-raw 'oldpassword=cafjjjbn0gpp5hq3dt40&password=test123'
-
Example Response
{ "data": null, "msg": "password change successful", "status": "success" }
Get all orders created in the store. Takes limit and offset as query parameters default values are 10 and 0 respectively Available to admin users only. Get all orders
-
Example Request
curl 'http://localhost:54545/api/v1/order/list?limit=1&offset=0' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=e4ecd3a4-b8be-493e-a33d-518ab11c65e8; max-age=300; path=/; secure; SameSite=Lax'
-
Example Response
{ "data": [ { "order_id": 1, "user_id": 3, "created_at": "2022-06-07 11:45:57.601996759 +0000 UTC", "modified_at": "2022-06-07 11:48:05.425488765 +0000 UTC", "status": "active", "currency": "USD", "order_number": "cafjktbn0gpp5hq3dt4g", "price_total": 1000, "discount_total": 0, "sub_total": 1000, "taxes_total": 0, "grand_total": 1000, "refunds_total": 0, "created_channel": "", "payment_provider": "" } ], "msg": "Order found", "status": "success" }
Get all users registered on the website. Takes limit and offset as query parameters default values are 10 and 0 respectively Available to admin users only.
-
Example Request
curl 'http://localhost:54545/api/v1/user/list?limit=1&offset=0' \ -H 'Content-Type: application/json; charset=utf-8' \ -H 'Cookie: session_id=e4ecd3a4-b8be-493e-a33d-518ab11c65e8; max-age=300; path=/; secure; SameSite=Lax'
-
Example Response
{ "data": [ { "user_id": 1, "email": "[email protected]", "created_at": "2022-06-07 10:53:00.480128762 +0000 UTC", "username": "", "last_password_set": "2022-06-07 10:53:00.480128762 +0000 UTC", "last_login": "2022-06-07 10:53:00.480128762 +0000 UTC", "verified": 0, "reset": 0, "user_role": "admin" }, { "user_id": 2, "email": "[email protected]", "created_at": "2022-06-07 10:53:00.532691788 +0000 UTC", "username": "", "last_password_set": "2022-06-07 10:53:00.532691788 +0000 UTC", "last_login": "2022-06-07 10:53:00.532691788 +0000 UTC", "verified": 0, "reset": 0, "user_role": "guest" }, { "user_id": 3, "email": "[email protected]", "created_at": "2022-06-07 11:13:06.947313364 +0000 UTC", "username": "", "last_password_set": "2022-06-07 11:13:06.947313364 +0000 UTC", "last_login": "2022-06-07 11:13:06.947313364 +0000 UTC", "verified": 2, "reset": 1, "user_role": "user" } ], "msg": "Order found", "status": "success" }
No, only binaries are released and site is used for discussions around the product.
It's in alpha stage functionality is complete however it may contain bugs
This was created because of the need to host courses
- Without giving up control by using online saas services for hosting course.
- Avoid using complicated solutions that require constant maintenance.
- Use same site generated by static site generators for landing page/blog and protected course content.
- Something simple to avoid maintenance overhead.
Not right now because subscription is not supported it is for one time digital products only. It also doesn't have route forwarding functionality where your own saas can be plugged in. However these changes can be added if there is sufficient interest plase start discussion if you would like to have these features as of now it is not on roadmap.
It can be used for hosting course and associated blogs. Blogs with newsletter. Blogs with premium content. Landing page of startup and associated blog. Selling themes etc.
linux and macos are supported out of the box. Windows users can use WSL however it is not tested.
Create issue and tag it with feature
This is not yet decided as of now it is free to use. Paid product if available will use separate channel. so if you are downloading from github release it is free forever. Help us in deciding it, tell us what you would pay for it in discussion board.
- Add sample scripts for running in aws/gcp/azure/alibabacloud etc.
- Add sample site to demonstrate server functionality
- Add performance data
- Add automatic backups
- Add support for subscription in case there is sufficient interest from community