Homepage

platformOs Modules

Last edit: Sep 19, 2024

Why we have built DevKit - our modular mini-framework for platformOS

Modular design is an approach to decompose complex systems into smaller, reusable building blocks to lower the complexity of the application.

It also enables the creation of reusable, loosely coupled software components, improving code maintainability, software stability, and time to market.

Most software solutions (content management systems, eCommerce solutions, CRM softwares) have three different layers:

  • Core: The main functionality and business logic maintained by the core team
  • Contrib/Vendor: Community templates, modules, and plugins
  • Custom: Project-specific logic and view layer maintained by the project developer

This structure makes it possible to start building your projects on a reliable, well-tested, stable core and extend the basic functionalities with third-party plugins and your own custom code.

Working with platformOS is a bit different.

platformOS gives you absolute control over your codebase: the app folder of your project is your territory. As a developer, you have complete freedom to implement any feature in the way that works best for you.

This freedom is one of the core principles of the platform. However, as your application grows in complexity, if you have not considered your application architecture and code design, it will become increasingly difficult to maintain and continue developing.

We wanted to provide a standardized framework and ecosystem that offers ready-made, extensible building blocks to improve the developer experience and speed to market. We also aim to recommend architecture patterns, conventions, and implementations that we have developed over the years while working on our platform.

Our modules

DevKit modules, which together can work as a mini-framework

This page lists official platformOS plugins developed by platformOS software developers.

  • core — The core module is the dependency for all other modules. It includes architecture patterns, defines conventions, and provides implementations for the most common functionality requirements, such as validation.
  • user — The user module is the starting building block for adding authentication and authorization to your application. It includes registration, signing in, password reset, and Role-based access control (RBAC) implementation.
  • payments — The pos-modules-payments (not to be confused with the legacy payments module!) provides an abstraction layer for payments, allowing the addition of concrete payment gateway implementations for actual processing, such as Stripe through pos-modules-payments-stripe.
  • tests — The tests module is a simple testing framework written in Liquid to make it easier to write unit-ish tests.
  • components - The pos-module-components implements DesignKit

Feature modules built using the DevKit framework

  • openai — The OpenAI module includes commands that serve as a bridge between the OpenAI Embeddings API (API key required) and platformOS Embeddings.
  • data-export-api — The Data Export API module provides API endpoints to allow third-party data agencies to consume data from your application and load it into their systems.

How to use DevKit

The heart of the framework is the the pos-module-core. It is documented in the GitHub repository. In addition to various utility functions and low-level implementations such as validations, it sets some rules and architecture patterns that make your application easier to maintain and develop, and make onboarding new developers faster. These rules are:

  • Never put any HTML inside a Page — Do not mix business rules with the presentation layer. Treat your pages as controllers; they should receive user input (through context.params), fetch all necessary data, and provide that data to the presentation layer defined inapp/views/partials.
  • Never invoke GraphQL from app/views/partials — Again, do not mix business rules with the presentation layer. If you need data to be rendered in a partial, you should fetch it in the Page and provide it to the view as an argument.
  • Encapsulate your business rules in Commands — This makes it easier to write unit-ish tests and to reuse code across your application.
  • Encapsulate your GraphQL queries in Query object - This makes it easier to re-use the query and allows you to encapsulate additional data processing if needed, provide extra validation for the query parameters, set defaults etc.
  • Use Events to broadcast that something has happened in the system to take additional action asynchronously — Following this pattern will make your code easier to maintain and understand, as well as provide necessary debug information in case the system does not behave as expected.
  • Follow Resourceful route naming convention to organize your Pages
  • Use Status implementation to keep track of resource's state Whenever you need to change a resource's status (for example, if you have a resource Article and would like to have states like draft and published to decide whether it should be publicly available), you can use this pattern to implement it. Usually, you will want to use it in combination with Events (to notify the system that the state has been changed, such as article_published, order_cancelled).
  • Use pos-module-users module for authentication and authorization — It provides not only a great starting point for registration, sign-in, and password reset features that most applications require nowadays, but also an RBAC implementation that you can extend to your needs.
  • Use pos-module-tests to add unit-ish tests to all your commands, minimizing regression bugs caused by modifying business logic.

Resourceful route naming convention

Constistency is one of the key factor when it comes to reducing learning curve and improving maintainbability of your application. We would like to propose following the resourceful route naming convention for your endpoints, even though there is no explicit concept of a resource in routing definition.

The convention can be best explained using an example. Let's assume we would like to create a web application that allows to create articles. Hence, the article will be our resource - to persist them, we will need to define a table for it in app/schema/article.yml. There are 7 typical actions that are required to define a full CRUD (Create Read Update Delete) experience. Resourceful route leverages HTTP method (GET / POST / PUT / DELETE) and the following url conventions to implement those actions:

| HTTP method | slug | page file path | description
|---|---||---|
| GET | /articles | app/views/pages/articles/index.liquid | display a list of articles (ideally by invoking a query object defined in app/lib/queries/articles/search.liquid)|
| GET | /articles/new | app/views/pages/articles/new.liquid | display a HTML form to create a new article |
| POST | /articles | app/views/pages/articles/create.liquid | persist article in the database (ideall by invoking a command defined in app/lib/commands/articles/create.liquid) |
| GET | /articles/:id | app/views/pages/articles/show.liquid | display an article which id is provided in the url (ideally by invoking a query object defined in app/lib/queries/articles/find.liquid) |
| GET | /articles/:id/edit | app/views/pages/articles/edit.liquid | displays a HTML form for editing article with a given id (ideally fetching input for the form will be obtained by re-using app/lib/queries/articles/find.liquid) |
| PATCH/PUT | /articles/:id | app/views/pages/articles/update.liquid | update a specific article which id was provided (ideally by invoking a command defined in app/lib/commands/articles/update.liquid) |
| DELETE | /articles/:id | app/views/pages/articles/delete.liquid | delete a specific article which id was provided (ideally by invoking a command defined in app/lib/commands/articles/delete.liquid) |

We recommend you make a best effort to try to map the functionality you are working on to the idea of aidea of a resource, which, if you are not used to it, might be a abstract at the beginning, but will pay out dividends in the future. For example, if you want to build a reset password functionality and send a user an email to reset their password, they could implement endpoints like users/reset_password and users/send_reset_password. Another way of thinking about it though would be to treat a password reset as a resource - and come up with a password_resets/new and password_resets/create endpoints. For this one scenario it might not make such a big difference in terms of the complexity, but if developers keep doing it, you might end up with multiple endpoints without a clue what they do unless you read the code, and finding the functionality written by someone else (or yourself from the past) will get harder and harder.

To quickly generate the whole CRUD for a new resource, you can use core module's CRUD generator that leverages pos-cli generate. For example, assuming you have download the core module via pos-cli modules download core in your project root directory, you should be able to generate CRUD for articles using the following command:

pos-cli generate run modules/core/generators/crud article title:string body:string published_at:datetime --include-views

which will generate all of the following files, that follow the convention described above:

create app/translations/en/articles.yml
create app/schema/article.yml
create app/graphql/articles/create.graphql
create app/graphql/articles/delete.graphql
create app/graphql/articles/search.graphql
create app/graphql/articles/update.graphql
create app/lib/queries/articles/find.liquid
create app/lib/queries/articles/search.liquid
create app/lib/commands/articles/create.liquid
create app/lib/commands/articles/delete.liquid
create app/lib/commands/articles/update.liquid
create app/lib/commands/articles/create/build.liquid
create app/lib/commands/articles/create/check.liquid
create app/lib/commands/articles/delete/check.liquid
create app/lib/commands/articles/update/build.liquid
create app/lib/commands/articles/update/check.liquid
create app/views/pages/articles/create.liquid
create app/views/pages/articles/delete.liquid
create app/views/pages/articles/edit.liquid
create app/views/pages/articles/index.liquid
create app/views/pages/articles/new.liquid
create app/views/pages/articles/show.liquid
create app/views/pages/articles/update.liquid
create app/views/partials/theme/simple/articles/edit.liquid
create app/views/partials/theme/simple/articles/empty_state.liquid
create app/views/partials/theme/simple/articles/form.liquid
create app/views/partials/theme/simple/articles/index.liquid
create app/views/partials/theme/simple/articles/new.liquid
create app/views/partials/theme/simple/articles/show.liquid
create app/views/partials/theme/simple/field_error.liquid

CI/CD on platformOS

We have created a repository to help you easily integrate with GitHub Actions and create a CI/CD pipeline: ci-repository-reserve-instance-url.

This repository provides code that allows you to create a "master instance" and configure it to have access to a pool of instances. You can then run tests for each open pull request on these instances. In your CI/CD pipeline, the master instance will be queried to ask for an URL and token to an available instance. It will mark the instance as reserved, and your pipeline will be able to release it as soon as it is no longer needed. A built-in safety check automatically releases the instance after a set number of minutes.

Questions?

We are always happy to help with any questions you may have.

contact us