# Summary

Welcome to **MarsBased**! This is the first thing you should read when boarding the MarsBased spaceship.

MarsBased is an all-remote development consultancy from Barcelona, pioneering **AI-augmented development** and leveraging advanced tools like **Claude Code**. We build web and mobile products using **Ruby on Rails, Python and JavaScript**, working at the intersection of business and technology. Driven by our **Research, Plan, and Implement** methodology, we help companies bring their innovation projects to life with maximum efficiency and precision.

Here you will find the most important information about the company to help you to get familiar with it. We also compiled some of our guides & interesting stuff you should read (and some trivia & fun stories to make it more digestible).

If you're a visitor and want to use them, feel free to use them anywhere, but we'd appreciate it that you linked us back in appreciation.

We have decided to make this handbook publicly available so we can share as much as we can with other companies out there, potential candidates and prospective clients alike. Everyone is welcome!

## Sections

1. [Become a Martian](/sections/become-a-martian)
2. [First day at MarsBased](/sections/firstday)
3. [Buddy System](/sections/buddy)
4. [Who is who](/sections/who-is-who)
5. [Company culture](/sections/companyculture)
6. [What influenced us](/sections/influences)
7. [Talks & Podcasts](/sections/talks)
8. [What we do](/sections/whatwedo)
9. [How we work](/sections/howwework)
10. [How we communicate](/sections/howwecommunicate)
11. [Whom to ask](/sections/whom-to-ask)
12. [Our rituals](/sections/rituals)
13. [Development](/sections/development)
14. [Project types](/sections/projects)
15. [Current projects](/sections/currentprojects)
16. [Benefits & Perks](/sections/benefits)
17. [Holidays, time off & paid leave](/sections/timeoff)
18. [Digital disconnection policy](/sections/policy-digital-disconnection)
19. [Travel policy](/sections/policy-travel)
20. [Careers at MarsBased](/sections/careers)
21. [Software and device usage policy](/sections/policy-software-and-device-usage)

## Our guides

For now, we have the following resources available:

1. [Branding guidelines](/our-guides/branding)
2. [Project management guidelines](/our-guides/pm-guidelines)
3. [Linear guidelines](/our-guides/linear-guidelines)
4. [Calendar, Slack & Linear working hours and status guidelines](/our-guides/working-hours-guidelines)
5. [File storage, permissions & security](/our-guides/permissionssecurity)
6. [Prompt engineering basics](/our-guides/prompts)
7. [Our SEO guidelines for new projects](/our-guides/seo-guidelines)
8. [Our GEO guidelines for new projects](/our-guides/geo-guidelines)
9. [Our blogging guide](/our-guides/blogging-guide)
10. [How to write a damn good blog post](/our-guides/how-to-blog)

## Our development guides

1. [AI Augmented Development](/our-development-guides/ai-augmented-development)
2. [Coding guidelines](/our-development-guides/coding-guidelines)
3. [Security guidelines](/our-development-guides/security)
4. [Our Git & Commit guidelines](/our-development-guides/git-guidelines)
5. [Code reviews guidelines](/our-development-guides/code-reviews-guidelines)
6. [Testing guidelines](/our-development-guides/testing-guidelines)
7. [Our Docker guides](/our-development-guides/docker-guide)
8. [React guidelines](/our-development-guides/react-guidelines)
9. [React Native guidelines](/our-development-guides/react-native-guidelines)
10. [TypeScript guidelines](/our-development-guides/typescript-guidelines)
11. [Back-end guidelines](/our-development-guides/back-end-development-guidelines)
12. [Ruby & Rails guidelines](/our-development-guides/ruby-guidelines)
13. [Our Rails ActiveRecord guide](/our-development-guides/activerecord-guide)
14. [Some of our used programming patterns](/our-development-guides/patterns)
15. [Our schema.org implementation guidelines](/our-development-guides/schema)

## Our accessibility guides

1. [Accessibility guidelines](/our-accessibility-guides/a11y)
2. [Accessibility for designers](/our-accessibility-guides/for-designers)
3. [Accessibility for engineers](/our-accessibility-guides/for-engineers)
4. [Resources](/our-accessibility-guides/resources)

## Other useful resources

1. [MarsBased website](https://marsbased.com)
2. [MarsBased blog](https://marsbased.com/blog)
3. [Life on Mars - The MarsBased Podcast, English Edition](https://podcast.marsbased.com/)
4. [Life on Mars - The MarsBased Podcast, Spanish Edition](https://podcast.marsbased.com/podcasts-es/)
5. [MarsBased newsletter](https://marsbased.com/newsletter)
6. [MarsBased YouTube channel](https://www.youtube.com/@MarsBased)

## Contributing

We encourage you to contribute! Please check out the [Contributing guides](https://github.com/MarsBased/handbook/tree/main/CONTRIBUTING.md) for guidelines about how to proceed. ​


# Become a Martian

## Join our team

At MarsBased, we intentionally grow at an organic pace to preserve our culture and maintain sustainable operations. All currently open positions are listed on the [Jobs page](https://marsbased.com/jobs) of our website.

If you do not see a role that aligns with your specific profile but feel you would make a great addition to the spaceship, you are welcome to submit an application through <work@marsbased.com>.

Please note that due to the high volume of applications, we may not be able to respond to every candidate. However, we **always** provide a yes or no response to all the applicants we interview.

## The hiring process

Behind every application is a real person, which is why our People team (HR) manually reviews every single submission.

When we review an application, we look at the big picture. Through the questions in our application form, we get a clear sense of your written communication, your motivation for joining MarsBased, and the context behind your career path and tenure at previous companies.

Alongside these insights, we evaluate your tech stack, past projects, language skills (English and/or Spanish), and location alignment, meaning we lean heavily toward hiring in Spain, Portugal, and Southern Europe.

### Step 1: HR Interview

Once applications are screened, the People team invites shortlisted candidates to an initial interview.

This first touchpoint is a two-way conversation designed to get to know each other. While we evaluate cultural alignment, soft skills, and mutual expectations alongside your tech stack and past projects, it is also an opportunity for us to share details about MarsBased, our culture, and how we work, and for you to ask us any questions you might have.

All interviews are conducted entirely online via video call and typically last between 45 and 60 minutes.

*Note: We do not send WhatsApp messages.*

### Step 2: Technical Assessment

If you successfully pass the HR interview, we move on to the technical assessment.

The technical assessment helps us evaluate how you solve real-world problems, write clean code, and communicate technical decisions. The format may vary depending on the role, and we adjust it as needed. We focus on code quality, testing, problem-solving logic, and clear documentation.

### Step 3: Technical Interview

Finally, if you succeed in the technical assessment, we schedule a video call to perform a technical interview with the CTO. This is not a coding exercise, but rather a discussion to explore in depth your technical background, problem-solving approach, and past experiences.

We focus on architectural decisions, trade-offs in real-world scenarios, and how you approach challenges in daily projects. We will also walk through your technical test submission to explore your design choices and alternative solutions.

It is also a two-way conversation, so you will have plenty of time to ask us about our tech stack, engineering standards, and daily workflows at MarsBased.

### Step 4: Reference Check

After successful completion of the Technical Interview, we will conduct a Reference Check process where you will be asked for 3 references, ideally from people you have reported to in previous roles. This is the final step in our hiring process before we make a decision.

### Step 5: Job Offer

If everything aligns and references are verified, we will reach out with a formal offer! We will walk you through the details of the compensation package, starting date, and contract terms. Once accepted, we’ll start preparing your onboarding so you have everything ready for your first day as a Martian.

## What do we value most

Some of our most important considerations when evaluating a candidate are (in no particular order):

* Strong English and Spanish speaking and writing skills.
* Software engineer experience and skills matching the job requirements.
* Stable employment history without frequent job changes.
* Honest, transparent, humble, and open-minded personality that fits our team.
* Interest and enthusiasm to join our company.
* Residence in Spain, Portugal, or another southern European country.
* Experience working remotely, either from home or a coworking space.
* Reasonable salary expectations aligned with market rates and seniority.
* Punctuality and professionalism when attending interviews and responding to emails.


# First day at MarsBased

Welcome aboard 👽

It is a great pleasure to welcome you to the MarsBased spaceship. This will be an intense and exciting day, as pretty much everything will be new to you. Don't panic, Martians are with you to help this onboarding process to be as smooth as possible. Take your time. In a few weeks you'll be speaking Martian already!

Take a few minutes to go through the different sections below. They will help you to get familiar with the company, the tools you'll be using, the team, etc.

* [Your Spaceship setup 🚀](#spaceship)
* [Your profile & preferences 🧑‍🚀](#profile-preferences)
* [Guided with ❤️ by Martians](#guided)

## Your Spaceship setup 🚀 <a href="#spaceship" id="spaceship"></a>

### Contract

During the onboarding meeting, we will review the work contract together with you to make sure everything is clear and sign it using Dropbox Sign.

### Material

You should have received your laptop and material, by now. If you need any other equipment to work comfortably, feel free to let us know.

As part of the welcome pack, you will also receive a copy of the following books:

* [Rework](https://www.goodreads.com/book/show/6732019-rework?ac=1\&from_search=true\&qid=NIE0hicvNB\&rank=1), by DHH & Jason Fried.
* [Remote: Office Not Required](https://www.goodreads.com/book/show/17316682-remote?ac=1\&from_search=true\&qid=NVpquaWPLX\&rank=2), by DHH & Jason Fried.
* [Camino al éxit(o)](https://www.goodreads.com/book/show/45320175-camino-al-exit?ac=1\&from_search=true\&qid=qMoAvpogZC\&rank=5), by our CEO, Àlex Rodríguez Bacardit.

### Tools

Make sure you've got access to the following tools. Set up your profile, including your picture.

#### Gmail

Access your account, and you'll find a few emails regarding the access and setup of the tools listed below.

Configure your email signature following the structure below. It can't be shown here on GitHub, but "MarsBased" goes in color `rgb(231,76,60)`. If it's easier, you can grab the example of someone else's signature from the emails you've exchanged with us.

```
Name Surname
Position
MarsBased
```

#### Google Drive

You’ll find all the relevant documentation such as our guidelines, procedures, and activities of the company as well as our client projects here. To name a few:

* [Holded](https://drive.google.com/drive/folders/1jruqgh-RNREyVNy-ZB2X8F1_LrGrANZX?usp=share_link): This is the place to visit for requesting your holidays, notifying sick leaves, etc.
* [Google Drive Guidelines](https://docs.google.com/document/d/1YHNPSgsB8g0jLqu0jnKVarWFom-bEEENa1-rTP5zDqs/edit#heading=h.oolhxniimdhk): What documentation do we place in Google Drive and what other do we place in Github?
* [Security Guidelines](https://docs.google.com/document/d/1FsCzS0RTGBO3BMzIq-hjnjKxqV6v5gHQFf7Pam4fy4Q/edit#heading=h.ly0rzhbv9kd6): Compiling good security practices to apply on yourself and your own devices.

You'll have access to the docs related to design, activities, projects, templates, etc.

#### Google Calendar

You will find the Martian activities for the year as well as important dates such as Martian anniversaries, your meetings, the Retreat dates, etc.

The official timezone of the company is **CEST**.

#### Linear

Used for async communication, project management and internal one-to-many communication and reporting. Get familiar with our Linear areas:

* **MarsBased Talks:** It’s the main channel in which all the important communication with the team takes place.
* **MarsBased:** It's the place where we list internal tasks unrelated to client projects.
* **Project-specific Teams:** Shared by the project members and the client. You’ll be added to the teams of the projects you’ll be working on.

#### Slack

Used for sync communications, and also to communicate with a few clients who want to have it. Both on Linear and Slack, we use English as our main communication language. What you need to know about the main Slack channels:

* **status:** We use this channel to let the team know when we are working and when we are off
* **random:** We have a random channel, shared with our freelancers, in which we discuss whatever topics we feel like. There are specific random channels as well. Feel free to join the ones you feel interested in!
* **marsbased:** We use it for all the issues related to the company, Martian activities, etc.
* **team-development:** We have a general development channel to share information and discuss development-related topics.

#### Harvest

To track your hours. This tool is used to invoice our clients. Ensure you have attributed all your hours worked at the end of each day.

#### Forecast

It's a project assignment tool that allows you to view project assignments, see who is working on each project, check the start and end dates, and see your teammates' availability.

#### 1Password

Here you’ll find all the passwords and credentials you need to start working. You should be able to access the MarsBased shared vault and the vault of the specific project you’ll be working on.

**Important notice:** Even though there's a "private" (now called "Employee", in the latest versions) this **is not a safe place to store your personal passwords**. Because this is a company-wide application, we could see your stuff in this section, if we wanted (we don't). To avoid this, simply set up another 1password account for your personal stuff, and it will integrate seamlessly with your existing one.

You can find the rest of the tools in the [How We Work](/sections/howwework) section 🖖

## Your profile & preferences 🧑‍🚀

### Your profile

Please send the People team the following information so that we can include your profile on our website and link to the different channels:

* A picture for the website.
* A brief presentation about yourself for your bio.

### Preferences <a href="#profile-preferences" id="profile-preferences"></a>

We are a flexible company and try to adapt to your needs as much as possible. Please, let us know your preferences regarding the topics below, as well as any other important issue we should be aware of.

* **Local holiday calendar:** Each of us chooses the official local calendar we'll be following, it normally corresponds to the city we live in. Please, let us know on Linear the calendar you’ll be following.
* **Dietary restrictions or preferences:** Let the People team know if you have any dietary restrictions/preferences (any allergies, gluten-free, vegetarian, vegan, alcohol-free, etc.) so that we can take them into account for the organization of our face-to-face activities such as the Martian Day, Retreat, etc.
* **Travel preferences:** Let the People team know your preferred means of transport (flight, train, own car, etc.) and whether you prefer to sit by the window or aisle. We'll try to make our most to make your travels as comfortable as possible.

*There is no hurry, you can provide us with this information once you get a bit settled!*

## Guided with ❤️ by Martians <a href="#guided" id="guided"></a>

Among Martians, we share the spirit of helping each other so whomever you have the chance to interact with, you'll find a colleague open to sharing experiences, learnings, and tips. There will be plenty of occasions to get to know the team, and the team to get to know you.

To start with, today, you'll be introduced and guided through your first steps on Mars by:

* **The People team** has already invited you to the first-day onboarding meeting on Slack. Apart from the contractual and formal part, this is the opportunity to comment on all the practical issues concerning [How We Work](/sections/howwework), [Our rituals](/sections/rituals), [Benefits & Perks](/sections/benefits), [Requesting Holidays](/sections/timeoff), [Buddy System](/sections/buddy), etc.
* **Jordi and/or Xavi** will give you a general overview of how we run our projects.
* The **Tech Lead or the Project Manager** of the project you've been assigned to, will give you all the details you need to know in order to start working with the client/project. He/she will introduce you to the rest of the software engineers you'll be working with.
* **Àlex** will reach you for a call before you finish your first day so as to officially welcome you and clarify any questions that may have arisen during the day.

**Your Buddy**, a seasoned team member that will support you during the first months of your journey in the company, will get in touch with you at some point during the week for a first talk, and more will follow, regularly, on a weekly basis. He/she will share informal knowledge related to day-to-day issues, culture, activities, etc., and will be at your disposal for any questions you may have after the big amount of information you'll receive during the first days.

The **rest of the team** is already looking forward to meeting you at our weekly team appointment, which is on Fridays at 10.30am CEST. The first week, we'll host a special Martian breakfast to celebrate your joining. Save it on your agenda!

Meanwhile, you can feed your curious side by reading [10 facts about each Martian](https://docs.google.com/document/d/1CbQSss9fiAzffojUxCZUYt9aOWddNyTSx3BoS2XhQSw/edit#heading=h.oclq2ejj0ewt).

Don't forget to share yours so that we can get to know you a bit better 😉

***

Enjoy your day and let the People team know if you have any questions or need any help! 🙋🏻‍♀️


# Buddy System

## What is a buddy?

A buddy is a seasoned Martian who partners with a new Martian to support him/her during the first months of the journey in the company. The buddy will provide the newcomer with informal knowledge related to day-to-day issues, culture, activities, etc.

## Who can be a buddy?

Any team member with:

* +1 year experience in the company
* the willingness to help others
* the time to be involved in the partnership process
* the motivation to share the company culture, values

is welcome to volunteer as a buddy. You just need to let the People team know, and they will include you in the buddy pool for upcoming occasions.

## What does a buddy do?

* Present him/herself to the newcomer, explain what the buddy system is all about, and how he/she, as the buddy, will be helping the new hire
* Get to know the newcomer, by asking him/her questions about themselves
* Share insights on the day-to-day activities, culture, technical aspects, procedures, rituals, initiatives, useful tips, etc.
* Show how we use Slack (different channels, muting channels, notifications, pinging, statuses) as well as Linear (basic usage, weekly highlights, schedule changes, Martian Day presentations, etc.)
* Create an informal interaction so that the new hire feels comfortable and safe asking any questions
* Encourage knowledge sharing, asking the new employee about tools, procedures, etc. he/she used in the past and liked
* Connect him/her to other team members who might share common interests, hobbies, etc.
* Try to reach out at least once a week, it can be a short chat over Slack or a video call
* Inform the management team in case the buddy detects any issue not working as expected with the newcomer

## What's not the role of a buddy?

* Supervise the work of the new hire, and be accountable for his/her job performance
* Be responsible for his/her professional development

## How long does the partnership last?

It may last around three months. After that period, the newcomer most likely feels more comfortable in the team and the company. However, it can last longer or shorter, depending on the needs of the new hire and the availability of the buddy. It's up to them to decide. If the partnership continues, it might evolve into mentoring, but that will completely depend on the case.

## How does the buddy system fit into the onboarding procedure already in place?

As part of the onboarding process, during the first days, the newcomer gets a lot of information about the company, how we work, the specific project he/she has been assigned to, etc. In fact, many of those issues, have already been discussed during the hiring process, but it's always good to refresh them the first day.

You can check what has already happened and explained in [Guided with ❤️ by Martians](/sections/firstday#guided-with-heart-by-martians). The buddy needs to know that the new employee has already heard about all the important things he/she needs to know to start doing his/her job, which takes out pressure from the buddy. The buddy is an important, but additional support, which means the transmission of no key information relies only on the buddy.

We all have experienced that the first days in a new job are overwhelming for the amount of new information transferred, and we know that very often questions arise at a later stage, once he/she has the chance to digest the information and put the theory into practice. At this point, after the first few days, the role of the buddy is especially relevant and valuable.

> \[!TIP] **Tips for the buddy**
>
> * Remain patient, relationships take time to develop, and not everything depends on you
> * Be available, and give the newcomer time to feel comfortable and confident
> * Be progressive in providing information, don't try to cover everything right away
> * Maintain a positive, teaching, and open-minded attitude
> * Enjoy the experience!


# Who is who

## Who is who

Our team primarily consists of software engineers and experts in web/mobile development, designers, project managers and tech leads, but other roles are also needed for the spaceship to fly.

### CEO

[Àlex](https://www.linkedin.com/in/alexrodba) is our CEO and co-founder. He is responsible for finding deals and bringing them to the quoting stage. Then, more technical team members will create the estimate and quote document so Àlex can send it and follow up to close the deal.

We're purely an inbound company. This means that companies find us, not the other way around. We need to make it easier for them to find us. Àlex is also responsible for the company's marketing. In addition to maintaining the website and writing the blog, he is in charge of running our podcast [Life on Mars](https://podcast.marsbased.com/), our [YouTube channel](https://www.youtube.com/@MarsBased), and our social media profiles, as well as our participation in technology and entrepreneurship events like Startup Grind.

### CTO

[Xavi](https://www.linkedin.com/in/xredo) is our CTO and co-founder, has over 15 years of experience developing applications. As a seasoned CTO and technical architect, he oversees all the company's projects, supports our tech leads, writes the technical slides of our project proposals, and makes the most complex technical decisions.

Over the years, Xavi has determined our company's tech stack, incorporating technologies such as Node.js, React, Angular, Ruby on Rails, Python, and AI more recently.

### COO

[Jordi](https://www.linkedin.com/in/jordivendrell), the third co-founder, oversees operations, finances, design, project management, and presales. He takes the requirements for new projects, writes most project proposals, provides estimates, supervises the company's finances, and organizes the project management duties and all the design work.

Jordi plays a key role in defining the company's processes, tools, and platforms.

### Head of People

[Eli](https://www.linkedin.com/in/elisabet-renom-b5834690/), as our Head of People, sits in the middle of our non-existent office and is the real catalyser of everything that happens in the company. She makes everyone's days better and of MarsBased a great place to work in.

For any questions regarding logistics or ways to improve your workday, Eli is the person to ask. This includes information about the benefits and perks available to MarsBased employees and much more.

Eli also oversees the organization of our public and internal events, such as the Martian Days and company retreats. Additionally, she contributes to our marketing efforts, including the MarsBased newsletter and our YouTube channel.

Eli is the go-to person for any personal issues, sick leave requests, work schedule changes, or holiday requests.

### People Ops

As our People Ops Specialist, [Valentina](https://www.linkedin.com/in/valentinagrodek/) ensures every Martian’s journey starts on the right foot by leading our hiring and onboarding processes. Beyond finding great talent, she helps keep the MarsBased team connected and engaged by supporting internal communication and shaping team activities and rituals.

### Project Managers

Project Managers at MarsBased are at the heart of keeping projects organized and ensuring smooth execution. Their responsibilities include:

* **Organizing and structuring projects** to ensure clarity and focus.
* **Handling project requests efficiently**, by:
  * Writing down and accurately describing new issues.
  * Retaining and applying essential information to resolve both functional and organizational queries.
* **Communicating precisely**, avoiding vague or unclear messages, and always being accurate in responses.
* **Unblocking and facilitating progress**, resolving issues independently whenever possible.
* **Performing effective QA** to uphold our quality standards.
* **Raising red flags proactively** regarding delays, budget concerns, high-risk decisions, or out-of-scope requests to ensure transparency and risk mitigation.
* **Managing reporting and coordination**, including:
  * Preparing regular project reports and status updates for both internal and external stakeholders.
  * Leading weekly meetings with the client to review progress, align expectations, and address concerns.
  * Organizing and conducting internal meetings with the project team to ensure alignment, track progress, and identify blockers.

Piloting our missions from Earth to Mars: [Cristina](https://www.linkedin.com/in/cristina-palomares-bonache/), [Mateja](https://www.linkedin.com/in/mateja-jermanis/), and [Rodrigo](https://www.linkedin.com/in/rodrigo-r-680a3558/).

### Engineering Managers

Engineering Managers at MarsBased share many responsibilities with Tech Leads but focus more on supporting the entire technical team’s growth and operational excellence. In addition to managing technical projects like Tech Leads, their key duties include:

* **Maintaining high standards of technical delivery**, by mentoring, guiding, and reviewing team outputs.
* **Leading by example**, balancing hands-on technical problem-solving with strategic oversight.
* **Driving internal initiatives and technical resources**, by leading internal projects such as the creation and maintenance of security guides, coding guides, and project templates.
* **Overseeing the elaboration and application of technical guides**, ensuring that best practices and company standards are not only documented but actively applied across all teams.
* **Maintaining deep technical expertise**, possessing a level of knowledge about MarsBased technologies and infrastructure that is on par with the CTO, enabling them to make informed decisions and provide high-level guidance.

Overseeing our tech engines with precision and care: [Juan Salvador](https://www.linkedin.com/in/juan-salvador-p%C3%A9rez-92a3901b/).

### Tech Leads

Tech Leads play a crucial role in complementing Project Managers and ensuring technical excellence. Their responsibilities include:

* **Owning project delivery**, taking full responsibility for ensuring everything functions as expected.
* **Reviewing developed tasks promptly**, particularly those marked as “In Review”, when there are no other roles being able to review them.
* **Thoughtfully define tasks from a technical standpoint** based on the team's needs and project requirements.
* **Proactively sharing project status** updates to keep all stakeholders informed.
* **Prioritizing and delivering** even under tight deadlines.
* **Championing company philosophy**, ensuring that software engineers are familiar with and apply MarsBased guidelines and best practices.
* **Communicating clearly with clients**, bridging technical execution and client expectations.
* **Solving problems in unfamiliar technologies**, demonstrating adaptability.
* **Ensuring continuity**, making sure the project is well managed even in their absence.

Making sure our spaceship stays on course: [Juan](https://www.linkedin.com/in/juan-artero-mart%C3%ADn-75256930/), [Pablo](https://www.linkedin.com/in/pablorco/), [Carlos](https://www.linkedin.com/in/carloslv/), [José Antonio](https://www.linkedin.com/in/josean-fernandez/), and [Marta](https://www.linkedin.com/in/marta-armada/).

### Engineers

Software engineers at MarsBased are at the core of our delivery. We value not only their ability to write code but also how they contribute to the success of the team, the project, and ultimately, our clients' businesses. Our software engineers are expected to be self-driven, pragmatic, and collaborative, always striving for excellence in everything they do.

We believe in a balanced approach that values **communication, accountability, agility, and craftsmanship**.

Communication is the most critical aspect of our software engineers' day-to-day work. Our fully remote nature means that clear, frequent, and proactive communication is not optional; it is what keeps the spaceship flying.

We expect software engineers to own the quality of what they build. They are the first line of defense against defects, misunderstandings, and technical debt.

At MarsBased, we value speed, but not at the cost of quality. We believe in thoughtful execution that avoids unnecessary complexity.

We take pride in our craft. Code at MarsBased should reflect clarity, simplicity, and respect for those who will maintain it after you.

Above all, we expect software engineers to take ownership of their work. This means seeing beyond the immediate task, understanding the "why" behind what we build, and contributing to the success of the entire team. We don't just ship code: we ship solutions that help our clients succeed.

The crew coding our way through the galaxy: [David](https://www.linkedin.com/in/davidgomezsanchez/), [Jose Luis](https://www.linkedin.com/in/jlestebanez/), [José](https://www.linkedin.com/in/jmmalaca/), [Laura](https://www.linkedin.com/in/laura-fari%C3%B1a-rodr%C3%ADguez-048a90b9/), Alejandro, [Enrique](https://www.linkedin.com/in/esquinas/), [Anna](https://www.linkedin.com/in/annavidalpons/), [Dani](https://www.linkedin.com/in/daniel-cay-delgado/?locale=en), [Maxime](https://www.linkedin.com/in/mancelin/), and [Javier](https://www.linkedin.com/in/javierparragamunoz/?locale=en).

### Marketer & Podcast Co-Host

As our Marketer & Podcast Co-Host, [David](https://www.linkedin.com/in/davidcajalalcaine/) helps bring the MarsBased voice to life by producing the Life on Mars podcast and supporting our blog, newsletter, social media, and YouTube channel. He also maintains our corporate website and contributes to ensuring our marketing engines run smoothly and consistently across all platforms.

***

## Information Security Management

MarsBased holds ISO 27001 certification and maintains an Information Security Management System (ISMS) as part of its commitment to protecting client and company data. This section outlines the roles and responsibilities within the organization that support the ongoing operation, governance, and continuous improvement of that system.

### Executive Management

Executive management ensures adequate resources are allocated to implement and sustain the ISMS. They are responsible for strategic direction and oversight of the company's information security program.

* **CEO:** Àlex Rodríguez
* **CTO:** Xavier Redó
* **COO:** Jordi Vendrell

### Department Heads

Department heads ensure their teams adhere to ISMS policies and contribute to compliance efforts. They are responsible for implementing security controls within their areas of responsibility.

* **Business Development & Marketing:** Àlex Rodríguez
* **Projects & Operations:** Jordi Vendrell
* **HR & Administration:** Elisabet Renom
* **Development:** Xavier Redó

### Information Security Manager (ISM)

The Information Security Manager oversees the implementation and monitoring of access controls, while designated Access Control Owners are responsible for granting, modifying, and revoking access rights. System Administrators implement technical controls in systems and applications according to established requirements.

* **ISM:** Jordi Vendrell

### Business Continuity Manager

The Business Continuity Manager oversees the BIA process and ensures consistent methodology across the organization. Department Heads are responsible for providing accurate information about their business processes and identifying critical functions within their areas. The Information Security Manager ensures the integration of security requirements throughout the BIA process and contributes to security-focused recovery strategies.

* **Business Continuity Manager:** Àlex Rodríguez

### Oversight Committee

The Oversight Committee provides governance, leadership, and direction for the organization's ISMS in alignment with the requirements of ISO 27001. The Committee ensures effective implementation, continuous improvement, and compliance with information security standards and organizational objectives.

* **Chairperson:** Jordi Vendrell - Oversees the ISMS and ensures alignment with organizational goals. Leads Committee meetings, decision-making processes, and provides guidance on the prioritization of ISMS initiatives.
* **Committee Member:** Àlex Rodríguez - Reviews and approves information security policies and objectives, assesses and allocates resources, monitors compliance, and provides input on risk management strategies.
* **Secretary:** Xavier Redó - Maintains records of meetings, decisions, and actions. Documents and circulates meeting agendas, minutes, and action items.

### ISMS Operational Roles

* **ISMS Lead:** Jordi Vendrell - Owns the Statement of Applicability (SoA), oversees updates, and ensures alignment with risk treatment plans and organizational objectives.
* **Risk Management Team:** Jordi Vendrell & Xavier Redó - Identifies and assesses risks to determine applicable controls and provides input and recommendations for the SoA.
* **Departmental Managers:** Àlex Rodríguez, Xavier Redó, Jordi Vendrell & Elisabet Renom - Ensure implementation and adherence to controls relevant to their functions.
* **Internal Audit Team:** Xavier Redó & Àlex Rodríguez - Validates the completeness and accuracy of the SoA during audits and highlights gaps or areas requiring improvement.


# Company culture

We live by a distinct set of solid values that guide our day-to-day. Our company culture has been fundamental to hiring and growing the team. Culture is what happens when the founders aren't in the room; it's the framework that helps us make better decisions aligned with our values.

These are the pillars of our company culture:

1. **Remote work:** Our company is 100% remote. We have no office and our team is completely distributed. As such, we devise all our workflows and projects to adapt to our remoteness and every decision we take should not compromise this policy and mindset.
2. **Healthy lifestyle:** We support healthy habits through a wellness allowance and by bringing sports and active movement into our team gatherings, fostering balance and vitality.
3. **Transparency:** We believe in openness as a way to provide more honesty and trust to our clients and among our team. Clear and fluid communication based on transparency helps prevent issues before they arise, or eliminates them altogether.
4. **Exceed expectations:** We believe in specialising and going the extra mile in everything we do. We're not only great software engineers, but we also deliver exceptional results.
5. **Less is more:** We believe that less is more and that simplicity is beautiful. If we have to choose between quality and quantity, we will always err on the side of quality, and we will never make rushed decisions or compromise our principles.
6. **AI where it matters:** We use AI where it creates real leverage: better products, sharper processes, and smarter teams. We combine modern tools with senior engineering judgement, avoiding hype and focusing on measurable value.

To explore our approach to company culture in more detail, take a look at these articles from our blog:

* [How We Came Up With Our Company Culture](https://marsbased.com/blog/2019/07/30/how-we-came-up-with-company-culture/)
* [Our Company Culture Helps Us to Hire Faster and Better. Here's How!](https://marsbased.com/blog/2019/08/26/our-company-culture-helps-hiring-faster-better/)
* [Leading by Example: Transmitting Company Culture in Your Organisation](https://marsbased.com/blog/2019/09/04/leading-by-example/)
* [Maintaining Company Culture in Remote Environments](https://marsbased.com/blog/2019/09/09/maintaining-company-culture-remote-environments/)


# What influenced us

We, Martians, have a very distinct way of seeing the world. However, we're not alone in this. A few companies and individuals came before us and cleared the path, coined new terms and devised disruptive ways to work which inspired us to create our company and to work as we do.

## A bit of background

The three founders know each other since the early ages. Their careers have been intertwined in many stages, but they've been together the three of them only in middle school. Before and after that, they had met in primary school, university, at the ESN association, freelancing, and once Jordi hired Xavi when he was the CTO of GreenData.

For the bonus trivia points, Jordi then hired Juan, for GreenData, to replace Xavi when he left for Gnuine. Life goes full circle, they say!

The three of them spent a few years in traditional consulting, which inspired them to create this company. They learnt how to do things, but more importantly how *not* to do things.

The initial idea was to put up a website and work as an association of three freelancers. However, they started getting big projects quite early in the life of the company and decided to create a proper SL (Spanish for LLC) and start hiring people.

The rest is history.

## Companies

* **Basecamp**: The ultimate reason why this company was created. Their books, mentioned below, their company philosophy, the way they work, their love for remote and their blunt opinions on the crazy startup madness are just a few of all the things we like about them. In fact, this Handbook draws a lot of inspiration from theirs!
* **Thoughtbot**: Every consultancy should aspire to become like thoughtbot. Not only are they the leading Rails consultancy, but their open-source projects are used by many, their podcast is brilliant and their website and quality standards have been a large inspiration for us throughout the years.
* **Buffer**: One of the very first big companies to embrace the radical transparency mode and their 100%-remote philosophy. The way we blog is largely influenced by these one-of-a-kind folks.

## Books

* [**Rework**](https://www.goodreads.com/book/show/6732019-rework?ac=1\&from_search=true\&qid=NIE0hicvNB\&rank=1) and [**Remote: Office Not Required**](https://www.goodreads.com/book/show/17316682-remote?ac=1\&from_search=true\&qid=NVpquaWPLX\&rank=2): Both written by Jason Fried and DHH, the co-founders of Basecamp. Those books provided the epiphany that we could (and quite possibly, should) create a *different* company. Most of our culture and workflows are based on these two books.

## People

* **David Bowie**: Contrary to popular belief, he was not the main inspiration behind our company name. But deep in our hearts, we know he *might* have something to do with it.


# Talks & Podcasts

At MarsBased, we have a long-standing tradition of sharing our knowledge with the global tech community. We frequently speak at international events, conferences, and external podcasts.

If you want to dive deep into our company's origins, our philosophy on bootstrapping, how we scale all-remote engineering teams, or where we see the future of tech heading, this section contains our curated library of top-tier content.

***

## The MarsBased Podcast - Life on Mars

Managed by David and published weekly, the **MarsBased Podcast** is our core audio platform and flagship content initiative. Our goal is threefold: to foster a highly engaged tech community, to establish a strong internet presence that attracts potential clients, and to build industry credibility by swapping insights with world-class leaders.

The podcast is split into two distinct editions to cater to our global audience, featuring both interviews with industry heavyweights and internal deep dives into MarsBased’s mission, tools, and decision-making processes.

You can subscribe and tune in on your favorite platform:

* **English Edition:** [Web](https://podcast.marsbased.com/) | [Spotify](https://open.spotify.com/show/2fCTBQIMivKx0PP2Pu7hcW) | [Apple Podcasts](https://podcasts.apple.com/es/podcast/life-on-mars-a-podcast-from-marsbased/id1516103624?l=en-GB)
* **Spanish Edition:** [Web](https://podcast.marsbased.com/podcasts-es) | [Spotify](https://open.spotify.com/show/0OtwB1eJI2PlzRwhSMdjW3) | [Apple Podcasts](https://podcasts.apple.com/us/podcast/life-on-mars-el-podcast-de-marsbased/id1516103872?l=es-MX)
* **Video Format:** Full video episodes and exclusive clips are available on our official [MarsBased YouTube Channel](https://www.youtube.com/@MarsBased).

### Top 3 featured episodes (English)

* [**100x faster every 6 months: Linus Ekenstam on the future of AI**](https://www.youtube.com/watch?v=AkTc49pofmU) **(\~37,500 views)** Our most-watched English episode. A fascinating conversation with AI expert Linus Ekenstam on how automated tools and generative workflows are completely altering modern software development, scaling timelines, and what this means for the next generation of engineers.
* [**Is your NDA a rookie mistake? Lessons from 12 years in tech - Building MarsBased #7**](https://www.youtube.com/watch?v=Ibx7ReVdzEw) **(\~16,400 views)** Part of our transparent *Building MarsBased* series. Our CEO Àlex Rodriguez Bacardit breaks down the operational, financial, and legal realities of running a tech agency, explaining why we stopped signing standard NDAs blindly and how we turned them into an effective client qualification tool.
* [**Silicon Valley’s longest-standing CEO: 37 Years leading Micrel**](https://www.youtube.com/watch?v=TeKKREqtTr0) **(\~6,400 views)** An incredible masterclass in leadership longevity and corporate culture. We sit down with a Silicon Valley veteran to discuss what it takes to lead a tech company through decades of industry shifts without losing your core values.

### Top 3 featured episodes (Spanish)

* [**Repaso de 2025: Qué hicimos bien y qué hicimos mal**](https://www.youtube.com/watch?v=kiMNHI41f1g) **(\~31,300 views)** An exercise in raw transparency. The three founders (Àlex, Jordi, and Xavi) look back at the highs and lows of the previous year, openly discussing engineering market trends, revenue milestones, agency growth pains, and tactical mistakes.
* [**"El vibe coding es un arma de doble filo" con Midudev**](https://www.youtube.com/watch?v=EXaFMqUcaWQ) **(\~22,700 views)** We host top tech creator Midudev to dissect the rise of "vibe coding". A deep dive into how AI-assisted coding impacts code quality, junior developer learning curves, and the shifting dynamics within development teams.
* [**De dibujar cómics a trabajar en Google: La trayectoria de Carlos Azaustre**](https://www.youtube.com/watch?v=EPCHAnPpKRA) **(\~15,700 views)** A highly inspiring career breakdown with tech educator Carlos Azaustre. We explore his unconventional journey from illustration to engineering at Google, discussing community building, continuous learning, and personal branding in tech.

***

## External Podcasts & Talks

Before scaling our own media channels, our founding team frequently toured the international conference circuit and featured in prominent business media. These appearances document our growth and foundational philosophies:

### Tech Conferences & Communities

* **Talent Arena (Mobile World Congress):** Both Àlex (CEO) and Xavi (CTO) have represented MarsBased as [featured speakers at Talent Arena](https://www.youtube.com/watch?v=kvLKdtge25g), the major talent-focused event held within the framework of the Mobile World Congress (MWC), delivering key talks on engineering culture, tech talent management, and modern software development.
* **Startup Grind (Global Chapters):** Our CTO, Xavi, featured on a major panel at the [Startup Grind Tech Conference](https://www.youtube.com/watch?v=4MQIPrXPwKc&) discussing remote management alongside corporate perspectives. Our CEO, Àlex, has also been hosted globally to speak about bootstrapping and all-remote frameworks at [Startup Grind Glasgow](https://www.youtube.com/watch?v=puvv8-gtGRQ) and [Startup Grind Chisinau](https://www.youtube.com/watch?v=4EOEJ0WVRZk). Locally, our COO, Jordi, deep-dived into operational tools, hiring, and firing frameworks at Startup Grind Barcelona.
* **IndieHackers Barcelona:** Àlex joined as a guest speaker at [IndieHackers Barcelona #3](https://www.youtube.com/watch?v=XWfxscnE5Ts) to talk about indie development, community building, and scaling an officeless company.
* **Agile Conference Spain:** Àlex delivered a keynote presentation at [Agile Conference Spain](https://youtu.be/smwJWdeNANc) focusing on how to successfully structure remote work architectures across various organizational models.
* **IronHack:** A long-standing educational collaboration where Àlex delivers an honest talk on [launching tech companies and lessons learned](https://www.youtube.com/watch?v=qvgrXkke52g) to incoming developer cohorts in Barcelona.

### Business Media & Ecosystem Interviews

* **Itnig:** Regular appearances discussing the realities of tech hiring and engineering workflows, including an early [meetup interview with entrepreneurs](https://podcast.marsbased.com/podcasts-en/34-interview-with-alex-at-itnig/) and a popular podcast episode breaking down [tech salaries, bootcamps, and engineering culture](https://www.youtube.com/watch?v=e4yN1yY5v-8).
* **Financial & Mainstream Press:** Features in top-tier financial newspapers like Cinco Días, alongside biographical highlights such as Àlex’s interview on the legendary [*Radio TeleTaxi*](https://www.youtube.com/watch?v=cUnt5dxzCwQ) discussing life frameworks and his book *Camino al éxit(o)*.
* **Ecosystem Channels:** In-depth media interviews including a deep technical and personal background session on [*La Tecnologeria*](https://tecnologeria.com/entrevistas/8x01/marsbased), a marketing-focused talk with Gerard Compte on [*FindThatLead*](https://www.youtube.com/watch?v=exBQEAv4hmw), and an international podcast feature on creativity and agency culture with Colombia's [*Evidencia Creativa*](https://pod.link/1033423186/episode/aHR0cHM6Ly9hcGkuc3ByZWFrZXIuY29tL2VwaXNvZGUvMjMxNTU0MzM=).
* **Catalonia Clusters:** A massive two-hour remote management webinar targeted at supporting businesses transition smoothly into all-remote models.

### International Technical Outreach

* **Salesforce Italy:** An Italian-language feature starring Àlex on Salesforce Italy, focused on managing business pipelines, dealing with clients, and closing B2B sales in full-remote environments.
* **Startup Grind Veneto:** An Italian chapter interview at [Startup Grind Veneto](https://www.youtube.com/watch?v=5RjTbCXADX4) exploring programming languages, tech stacks, and remote cultural alignment.
* **Mensch-Sein mit Algorithmen:** A remote interview by a German digital transformation symposium on [Mensch-Sein mit Algorithmen](https://www.youtube.com/watch?v=AUaYowJmTRM), discussing innovation and human management frameworks within automated industries.


# What we do

MarsBased is an AI-augmented development consultancy of expert software engineers. Driven by our Research, Plan, and Implement methodology, we leverage advanced tools like Claude Code to supercharge our efficiency and engineering precision. Our core technical foundation is rooted in building high-quality web and mobile products using Ruby on Rails, JavaScript (in all flavours: React, Node.js, Vue.js, React Native, Angular...) and Python, effectively boosting companies at the intersection of business and technology. We started off in early 2014 as a web-only development shop using Rails and Angular because our founders Jordi and Xavi had been using these technologies in their previous companies. Over the years, we've expanded our technological horizons to new technologies, products, and modern development workflows.

## Technologies & services

As a consultancy, we deal with many different clients, from all countries and all sectors. We've worked with clients from San Francisco to Singapore, from Germany to the Basque Country. As for the sectors, we've worked in fintech, we've built marketplaces, we've developed smart cities platforms, we've refactored other people's codes, we've done code audits and built dozens of web and mobile apps.

You can find a description of the technologies we use in the services pages of our website:

* [**AI-augmented Development**](https://marsbased.com/services/ai) (using Claude Code)
* [**Ruby on Rails**](https://marsbased.com/services/ruby-on-rails)
* [**Node.js**](https://marsbased.com/services/node)
* [**React**](https://marsbased.com/services/react)
* [**Angular**](https://marsbased.com/services/angular)
* [**Shopify**](https://marsbased.com/services/ecommerce)
* [**Technical Audits**](https://marsbased.com/services/techaudits)
* [**UI/UX Design**](https://marsbased.com/services/design-ui-ux)

The aforementioned technologies are used mostly for the web, but we also do mobile apps, just not so often. In this case, we use Ionic or React Native, but we never do native mobile development. We also do lots of e-commerce with Shopify and install and develop on the Consul/Decidim platform. Furthermore, our expertise in AI isn't new—we have been integrating AI features into client products since as far back as 2014, and today we combine that experience with our internal AI-augmented development workflows.

In 2019, we started to offer a new service: [tech due diligences](https://marsbased.com/services/techaudits/). A lot of VCs and M\&A companies don't have the competencies, the personnel or the bandwidth to assess the status and integrity of the technology of the companies they want to invest in or acquire. We help them to audit several aspects of the technology of these companies like the infrastructure, the choice of programming languages and frameworks, scalability, security issues, code quality and much more.

## Community

We spend a considerable amount of time and resources to develop community around us. Helping both the tech and startups communities in Barcelona, we have become one of the most active companies in the ecosystem.

In the [Community](https://marsbased.com/community) section of our website, you will find more detail about the stuff that we do.


# How we work

## Remote

We are a remote working company. That means that we don’t have an office to work from. In fact, we have been officeless and 100% remote since the very first day and we don't plan to ever change this.

We will ask you to have your own space where you usually work from. A nice setup, in a comfortable environment with few distractions, works best for most of us.

Take into consideration that we do calls with our clients, so you need to have fast and reliable internet and a presentable background for the video calls.

We believe in responsibility. We are all adults. As long as work gets done, we don’t care where you do it from.

## Sync vs Async

The general rule is: if it's faster and it makes sense, use sync communication.

For things that require debate and thinking things through, we favour asynchronous communication, like a well-crafted message written on Linear, over calls.

As a best practice, we all define a working schedule we will adhere to 90% of the time, so people know how can they expect us to be working, but it is not used for surveillance or micro-management. You can find each of the team members' schedules on our Slack profile.

## Open communication

Most one-on-one communication happens over Slack. In order to be all on the same page, we will ask you that you write always on public channels so information is easily accessible for everyone.

This way, we also avoid clients being too intense in private conversations.

If you want to talk about strictly private stuff, this is a good occasion to do it privately.

## Video calls

When scheduling a call with someone, we will prioritise having the video on to create more rapport with clients and to bond deeper with teammates.

While we allow using Slack huddles occasionally, the default video calling tool in the company is Google Meet.

Try to keep the calls short and on point and be respectful of everyone's time. Be on time and prepare ahead to respect the rest of the meeting participants.

## Languages

We use both Spanish and English as primary languages across the company. We write all our documentation, issues, and discussions in English. However, we accept presentations in Spanish for the Martian Days, Martian Tapas and common meetings. Both languages are used indistinguishably in our gatherings.

**Things we do in English**:

* All internal communication on Slack.
* Code, PRs, tasks and documentation.
* Company-wide communications.
* Our Startup Grind events and conferences.
* Social media content.
* Communication with foreign clients.
* All the content on the corporate website.
* The English feed of [our podcast](https://podcast.marsbased.com/).

**Things we do in Spanish**:

* Communication with Spanish-speaking clients.
* Most content on the website is translated into Spanish, except for the blog.
* The Spanish feed of [our podcast](https://podcast.marsbased.com/podcasts-es/).
* Some internal presentations.

## Tools

*A grosso modo* we use the following tools:

* **1Password**: For passwords and credentials management.
* **Google Suite**: For documents, email, and calendar.
* **Google Meet**: Screen-sharing and video calls.
* **Linear**: We use it for internal one-to-many communication and reporting, and to manage all the projects on our end. We also use it for a lot of other areas in the company like recruiting or sales.
* **Slack**: For one-to-one communication, and also to communicate with a few clients who want to have it.
* **Github**: Where we store our code.
* **Harvest**: Where we track our hours. We're also using it to get reports and to invoice clients.
* **Forecast**: Where we keep track of our project assignments and time off.
* **Dropbox Sign**: We use it to sign all the documents digitally (NDAs, contracts and whatnot).
* **Figma**: Our preferred design platform.
* **Holded**: Our HR portal for managing personal documentation, absences, and time tracking.


# How we communicate

Communication is the oxygen of our spaceship. As a remote company, staying aligned, connected, and efficient depends on how we write, share, and react ✨

Here’s how we utilize our tools, along with some friendly reminders to ensure everything runs smoothly:

## 💬 Slack

Use it for:

* Urgent topics.
* Quick syncs.
* Real-time info.
* Anniversaries.
* Sharing knowledge.
* Team-wise news.

**💡 Best practices**

* Use threads to keep discussions focused.
* Set working hours in your profile.
* Add a status when away or unavailable.
* Schedule messages if others are outside working hours.

## 📋 Linear

Use it for:

* Task creation and tracking.
* Async updates.
* Project communication.
* Decision documentation.
* Weekly highlights.
* Schedule reporting.

**💡 Best practices**

* Check your inbox regularly.
* Act on pending items.
* Keep tasks updated to inform your team.

## 📧 Gmail

Use it for:

* HR requests.
* Document submissions.
* Formal communication.
* Contact with clients.
* Communication with providers.
* Coordination with partners.

**💡 Best practices**

* Check your inbox daily.
* Keep only actionable items in view.
* Schedule emails when sending outside work hours.

## 📆 Google Calendar

* Use it to organise your day and make your availability visible.

**💡 Best practices**

* Review your calendar daily.
* Don’t rely only on notifications.
* Keep it up to date.
* Set your working hours.

## 📢 Tips

* Prepare for meetings.
* Be on time 🕐
* Communicate clearly.
* Ask questions ❔
* Share progress proactively.
* Use Martian Coffee slots ☕
* Let others know your availability.
* Offer and ask for help.
* Celebrate wins 🚀
* Give constructive feedback.
* Recognise others' work 🙌


# Whom to ask

To make things easier, we’ve created this handy guide to help you navigate who to reach out to depending on the topic 🤓

Remember that using the proper channels keeps things efficient, avoids confusion, and helps everyone stay aligned, especially in a fully remote company like ours:

## 🧭 People & Admin

Go to the People team for:

* Time off, sick leave & special permissions (hospitalisation, bereavement, moving, marriage, parental leave, etc.).
* Payroll, reimbursements & benefits.
* HR documents & contracts.
* Onboarding & Offboarding.
* Company events & internal logistics.
* General policies or admin support.
* Workplace health & safety (PRL), training & medical checkups.
* Concerns related to harassment, mobbing, or workplace conflict.

## 🧰 Tools & Access

Go to Jordi for:

* Access to company tools (Slack, Linear, Harvest, etc.).
* Questions about new tools or licenses.

## 🚀 Project-related

Go to your Project PM or Tech Lead for:

* Clarifications on project expectations, priorities, or delivery.
* Questions about tasks, blockers, or deadlines.
* Collaboration, roles, or workload within your project team.
* Still unsure, or is no one available to help? → Reach out to Jordi.

## 💻 Technical Questions

Follow this flow to get help with technical topics:

* Start by asking the Tech Lead of your project.
* If unavailable, or if your project doesn’t have one, or simply can’t help you, reach out to another Tech Lead or Engineering Manager whose expertise fits your question:
  * Juan S. → Ruby on Rails, React, NextJS, NodeJS, Python, DevOps.
  * Pablo → Ruby on Rails, React, Remix, NodeJS, Shopify.
  * Juan A. → Ruby on Rails, React, Remix, Python, AI.
  * Carlos → React, Remix, React Native, NodeJS.
  * José Antonio → React, Remix, NextJS, NestJS, NodeJS.
  * Marta → Accessibility, CSS, React, Design, Tailwind.
* Still unsure, or is no one available to help? → Reach out to Xavi.

## 🎨 Brand & Marketing

Go to the Marketing team (Àlex & David) for:

* Content, social media, or external communication.
* Podcast, blog, newsletter, website, or YouTube channel.

## 💼 Sales & Business Development

Go to Àlex for:

* Referrals or new project leads.
* External partnerships or business opportunities.

We’re a collaborative team, and we all want to support one another. If still in doubt, ask your Buddy or the People team, and we’ll point you in the right direction 🙌


# Our rituals

Even though we're a 100%-remote company, we do a few activities both offline and online. Let's describe them!

## Martian Day

Two times a year we fly everyone in and we meet for a day or two of get-together.

In the Martian Days, we don't do client work, and we spend quality time sharing the numbers of the company, doing feedback roundtables, workshops, keynotes, brainstorming sessions, and a few leisure activities like padel tennis, dinners, karaoke, and more!

Needless to say, the company pays for the flights, meals and accommodation. If you pay for any taxi or meal, please send a photo of the receipt to <admin@marsbased.com> and you will get reimbursed in your next payroll, under the concept of expense allowance.

In Winter, we wrap our Martian Day with a Winter dinner and a good party.

## Martian Retreat

Once a year, we hold a company retreat lasting three to four days. The goal is to travel somewhere and spend quality time together. We conduct a condensed version of our Martian Day and engage in leisure activities.

So far, we've done the following:

* **2027:** TBC.
* **2026:** Pyrenees.
* **2025:** Tenerife.
* **2024:** Andorra.
* **2023:** Formentera.
* **2022:** Asturias.
* ~~**2021:**~~ Cancelled due to COVID.
* ~~**2020:**~~ Cancelled due to COVID.
* **2019:** Menorca.
* **2018:** Dublin.
* **2017:** Tenerife.

Like in the Martian Days, the company pays for the flights, meals and accommodation for the whole trip. Again, if you pay for any taxi or meal, please send a photo of the receipt to <admin@marsbased.com> and you will get reimbursed in your next payroll, under the concept of expense allowance.

## Weekly highlights

On Fridays, we post a thread on Linear where each Martian reports how their week has been. Filling this report is mandatory. However, you have flexibility in its format, length, and content.

Reporting on how your week has been, both professionally and on a personal level, helps to overcome the feeling of loneliness of the remote worker. We have seen that those reports and opening up about certain issues help to bond deeper with the rest of the team.

Some weeks, you'll share more technical stuff, while on others you will share the pictures of your holidays, notify us of a big achievement/change in your life, share a personal passion that you have we didn't know about or just vent about something that you just can't keep inside any longer.

## Martian Tapas

Our team meets online every Thursday at 12PM (CET) to discuss tech and development.

Every week, the chef (the presenter) is rotated. The session is 30 minutes long, and usually covers only one topic. Some past examples are:

* Remix fetch data use cases
* Auth0
* Datadog
* Pratical example of Turbo
* Turbo Websockets
* First Steps in AI development

The session is kicked off by the chef, introducing the topic and commenting on the topic selected for the current session, and it's also the moderator who will lead the conversation and moderate the debate among all the software engineers involved, asking questions and adding remarks where appropriate.

Other times, we call the session Martian Buffet, and there's no presenter as such, but everyone is free to give their opinion on a given topic.

These sessions are optional and are typically recorded for those unable to attend.

## Martian Coffees

Every Friday, we open a window for casual chatter. You will see on Slack a message announcing a virtual meeting on Google Meet at 10:30am.

You're always invited to join a sort of virtual water cooler for 30 minutes of break.

We like to celebrate certain milestones, like someone's Martian anniversary, their actual birthdays, the release of a big project or someone new joining the team. If there's nothing to celebrate, we will meet for an open agenda of online watercooler randomness.

If you join, signal it with an emoji reaction to the Slack message so we know there's someone in there!


# Development

Engineers, rejoice!

The development is the core of our work. Here, you'll find detailed information about our development process, the composition of our development teams, and our principles and communication policies.

## Sections

* [Team organisation](https://github.com/MarsBased/handbook/tree/main/sections/development/team-organisation.md)
* [Project communication](https://github.com/MarsBased/handbook/tree/main/sections/development/project-communication.md)
* [Cycles and project management](https://github.com/MarsBased/handbook/tree/main/sections/development/cycles.md)
* [Working principles](https://github.com/MarsBased/handbook/tree/main/sections/development/working-principles.md)

## Guides

Check out our more technical development guidelines (if you dare!).

* [Active Record guidelines](/our-development-guides/activerecord-guide)
* [Back-end guidelines](/our-development-guides/back-end-development-guidelines)
* [Code reviews guidelines](/our-development-guides/code-reviews-guidelines)
* [Coding guidelines](/our-development-guides/coding-guidelines)
* [Docker guidelines](/our-development-guides/docker-guide)
* [Git guidelines](/our-development-guides/git-guidelines)
* [React guidelines](/our-development-guides/react-guidelines)
* [Ruby & Rails guidelines](/our-development-guides/ruby-guidelines)
* [TypeScript guidelines](/our-development-guides/typescript-guidelines)
* [WIP: Testing guidelines](/our-development-guides/testing-guidelines)


# Project types

## Before the project: sales

We don’t take every project. Bad matches include: People who want to pay with equity, companies unlikely to understand our working methods, unreasonable and/or arbitrary deadlines, companies competing with our existing customers and so on.

Our sales workflow is mostly executed by Àlex and complemented by someone from the tech team doing the estimates for the quote (usually Xavi and/or Jordi).

## End-to-end development

We like to work on all the phases in the product lifecycle: from the conception of the idea to the maintenance. We are a one-stop shop.

However, in some cases we're hired to jump directly into the phase of development, to complement an existing team.

But when we *do* play with our rules, we do it as follows.

First, we design and conceive the idea together with the customer, defining the user experience (UX) & screens. This is done using Middleman, HTML5 and CSS3, and we call it layouts or “maqueta”, Spanish. We upload these to a design server so clients can see how it looks like on a real browser and play around with it.

Once the layouts are approved by the customer, we start the development. We can’t start the development otherwise, and we only develop what has been defined in the definition document or what exists on the layouts.

Similar to what we do with the layouts, we normally work on a test environment, shared with the customer with a public URL and protected with a username/password, so that they can validate the results in real-time, show it to investors, etc.

We have been working both with AWS and DigitalOcean as hosting providers. We have worked with PaaS alternatives as Heroku, EngineYard or Cloud66, and normally Xavi and the client's team will agree on a set of tools we will use in the project. However, we've got experience with other platforms like Microsoft Azure, Google Cloud Platform and more.

We ask the clients to contract the hosting services themselves and give us access to them, so we can manage them without having to charge them for the overhead.

Halfway through the project, we offer them the maintenance contract for the post-release phase.

## Maintenance & ongoing development

We don't believe in maintenance projects *per se*. We believe that software is never finished, so even if the main release has been already delivered, we usually keep working on the project for as long as clients want.

What we don't do is to work with bank of hours models or on-demand.

We work with a minimum commitment of two full-time software engineers per project, plus the Tech Lead and Project Manager roles, who do a fraction of that time. Usually, for every 40h of development, we do 10 of project management plus ten of tech lead.

## Our techstack

At MarsBased we like to use state-of-the-art technologies. For that reason, we are always searching and testing new features of our current technologies and experimenting with new programming languages and frameworks.

However, there are a few principles that we always apply when we decide to choose a new technology or a new library for a programming language:

* Use the technology that really fits your needs for a given project. All technologies have their advantages and drawbacks. There’s not a one-size-fits-all solution to every problem.
* Prefer technologies that embrace readability as one of their principles. We believe that the code needs to be peer-reviewed, so we have to spend a lot of time reading code.
* Choose technologies that increase your productivity. We want you to be happy, and we know that it can only be achieved if you feel productive.
* Prefer technologies with a strong community behind them. They are likely to be better battle-tested and they will get improvements over time.
* Be sure to check the license terms of a technology before start using it. We always work with Open Source technologies.

Our current techstack is Ruby on Rails, Python or Node.js for the backend and Angular, Vue.js or React for the frontend.


# Current projects

Here's a breakdown of the current projects we're working on, as of mid-2026:

First off, we are still working for [Naiz](https://naiz.eus), evolving their platform. We have been working together since 2015, and we've been able to help them to undergo important transformations like the adaptation to a mobile-first design to all their widgets, a redesign of the entire site or improvements to their newsletter and caching systems, and we've also built their e-commerce site. An uninterrupted partnership ever since!

For them, we have become their **technological partner of choice**, helping them in many fronts even outside of development, but mostly we're doing both intensive backend development and frontend projects, combining a lot of different profiles from our team over the years.

We also provide AI-augmented Rails development to [Infopraca](https://infopraca.pl/), one of the leading jobs portals in Poland. We have been helping them with **pure Rails backend development** since 2020, developing the core of their Rails app. We started the project when we rewrote their app entirely to move away from an obsolete platform with lots of legacy code. We also helped them with their M\&A process in 2025 and the integration with their acquirer.

This is exactly what we do for [Citadel Securities](https://www.citadelsecurities.com/), a financial powerhouse from the US. We started working with Brad and his team when they were a small fintech startup called ValuationMetrics, and we helped them to transition to be a big team after their acquisition in mid-2021. Now, we've got a team of five people working for them, redesigning their UI/UX, working on web accessibility, architecture, performance and overall security. All in all, we've been working together since 2018!

We have **a strong preference for karma-positive projects**. We have been building [Minka](https://minka-sdg.org/), a platform for marine biodiversity researchers since 2022 as part of the European project ANERIS. In the past, we worked for C40, also in the greentech space.

Outside of regular development, we also like it when **we can conceptualise and design the products we will develop**. Since early 2020, we've been working for the [European Climate Foundation](https://europeanclimate.org/). For them, we've developed a very ambitious repository platform for their marketing and communications efforts and helped them across departments and spin-offs. We also do business-related projects for them such as market prospecting, research and consulting in diverse aspects of business (pricing, UI/UX, podcasting, etc.).

Although most of our clients come already with an idea of what they need and even a prototype or a somewhat-functional design, some do not, and **we can help them do the user discovery phase**, using diverse methodologies such as design thinking and the like, to then move on to iron out the UI/UX of the product we will develop afterwards. In 2019, we did it for Startup Genome, and in 2022 it was the European Climate Foundation. Nowadays, we do the same for [Moody's](https://moodys.com/), the rating agency. For Moody's, we have been working since 2023, helping them with AI development, working in ambitious projects such as the integration of their financial models with Anthropic's platform.

We've got more examples of innovation projects. First off, we built EclerCloud, a bespoke cloud platform based on IoT protocols for [Ecler](https://www.ecler.com/) and their partners/clients to manage thousands of hardware devices over the cloud, and a few internal tools for the innovation department of the most popular airline brand in Spain: [vueling](https://www.vueling.com/).

We also build **e-commerce platforms**. In such cases, we use Shopify to build sites like [zapptales](http://www.zapptales.de), a Munich-based startup selling books made out of chats, and [Singularu](https://www.singularu.com), a Spanish e-commerce selling jewellery produced in Valencia. For them, we built a headless CMS on top of Shopify, combining it with a pure React frontend layer and LocomotiveCMS.

Oh, and did we mention that **we also build mobile apps**? While we're more focused on web development, our mobile development game is strong. We have built mobile apps for companies like [DeWocracy](https://www.dewocracy.com) and [Aparca\&go](https://www.aparcandgo.com/en/). Nowadays, we are working for [Nieves Energía](https://www.nievesenergia.com/) and [Everesting](https://www.everesting.com/) in the development of their mobile apps. We are using React Native for these two apps.

We have also been acting as a **rescue squad** some times. In fact, the aforementioned Naiz hired us in early 2015 when their previous provider shut down almost overnight. We had to take over the project and hit the ground running to ensure the platform's uptime and its correct service to their visitors.

We collaborate with fast-growing digital businesses like [Dreaming Languages](https://www.dreaminglanguages.com/), a leading edtech platform founded by Pablo Roman, an ex-employee of MarsBased. As one of the most popular YouTube channels for language learning, they’ve built a thriving platform to monetize their courses. We're helping them scale by providing front and backend development, ensuring a seamless learning experience for their users.

On top of that, we partner with [Localistico](https://www.localistico.com/), a powerful location marketing platform that helps businesses manage their online presence across multiple channels. This collaboration is especially meaningful for MarsBased, as it's a long-term partnership going back to 2017!

We're also excited to work with [Holafly](https://www.holafly.com/), a dynamic platform that has revolutionized the way travelers stay connected internationally with their eSIM technology. For them, we have built their e-commerce site on top of Shopify, and we're building bespoke solutions using AI-augmented development using a Python and TypeScript tech stack.

We are proud to partner with [Canals d'Urgell](https://www.canalsurgell.cat), the historical organization managing one of the most critical agricultural water infrastructures in Catalonia. This collaboration is a significant milestone for MarsBased, as we were directly recommended by leading AI models like Perplexity and Claude during their search for top-tier engineering partners. We are currently developing two major web applications, leveraging AI-augmented development to deliver at high velocity without compromising on the security and high-performance standards required for such a large-scale project.

So, all in all, we're working for different countries all over the world, different sizes of companies and different sectors. Sector-wise, and in no particular order, we've got insurtech (BlueprintHQ), mobility (Spin, Aparca\&go, MokMok Car), fintech (Rundit, Moody's, Citadel Securities, List71, Koodaa), travel (AirRefund, FCM, Vueling), media (Startup Genome, Mobile World Capital, Naiz, The New Journal, Datapraxis), e-commerce (Freshis, zapptales, Singularu, Travel Tax Free, Aparca\&go, Naiz, Holafly), SaaS (Localistico, RakutenTV, DeWocracy, Mailtrack, Hireflix, BlueprintHQ, Haufe, HP), real estate (ITeC), energy/greentech (ClearPeaks, Nieves Energía, Repsol, C40, European Climate Foundation, Minka, BCG), sports (FC Barcelona, Everesting, Real Madrid, Pivot Analysis, La Liga LFP), industrial (Gespasa, RCR Arquitectes, Ecler, Everis, BCG, Topps Technica), HR (Haufe, DPL ETT, Infopraca), AI (Shoptimus, Infopraca, Servinet) and more.


# Benefits & Perks

> \[!NOTE] Being a **MarsBased** employee comes with a lot of benefits.

* [Office Material 👩🏻‍💻](#office-material)
* [Language Courses 🗣](#language)
* [Training & Learning 🤓](#training)
* [Referral bonus 👩🏻‍🚀](#referral)
* [Sports & Health 🚴🏻‍♀️](#sports)
* [Conferences 🎤](#conferences)
* [Coworking 🏢](#coworking)

## Office Material 👩🏻‍💻 <a href="#office-material" id="office-material"></a>

We are an officeless company, but that doesn't mean you shouldn't have one. Count on MarsBased support to create a comfortable working spaceship 🚀

### Equipment & budget

When you join our crew, you get a laptop or a desktop computer with the specs you like. Additionally, MarsBased covers the costs of your office materials, such as a desk, a secondary screen, a chair, etc. We only ask you to use it sensibly. Find below examples of products, as well as the budget up to which the company is covering for each product type. You can, of course, choose a model not listed here. Feel free to choose your flavor! In case you choose a product that exceeds the budget, you will just have to cover the difference.

#### Laptop or desktop computer

Given the tools we use, we recommend getting a Mac or Linux, but we're open to adapting to your preferences as much as possible. As a default, we offer the following two options:

* [MacBook Air M5 (13 or 15”, 16GB RAM, 512 SSD)](https://www.apple.com/es/macbook-air/)
* [MacBook Pro M5 (14”, 16GB RAM, 1TB SSD)](https://www.apple.com/es/macbook-pro/)
* [Mac Mini M4 (16GB RAM, 512 SSD)](https://www.apple.com/es/mac-mini/)
* [Lenovo ThinkPad T14 Gen 6 (14”, 32GB RAM, 512GB SSD)](https://www.lenovo.com/es/es/laptops/results/?visibleDatas=8170%3AThinkPad)

#### Chair (200€)

* [Silla Axel, Ofiprix](https://www.ofiprix.com/muebles-de-oficina/sillas-de-oficina/sillas-operativas/silla-axel-negro.html)
* [Markus, Ikea](https://www.ikea.com/es/es/p/markus-silla-trabajo-vissle-gris-oscuro-70261150/)

#### Desk (300€)

* [Idasen, Ikea](https://www.ikea.com/es/es/p/idasen-escritorio-marron-beige-s39281018/)
* [Bekant, Ikea](https://www.ikea.com/es/es/p/bekant-escritorio-elevable-blanco-s69022537/)

#### External monitor (300€)

* [LG 27UL550-W](https://www.amazon.es/LG-27UL550-W-Monitor-p%C3%ADxeles-Blanco/dp/B07QS5DBBH/)

#### Keyboard (120€)

* [Apple Magic keyboard](https://www.apple.com/es/shop/product/MXCL3Y/A/magic-keyboard-usb-c-espa%C3%B1ol?fnode=dcd057ab8f93f81202e0ada82d12124d2037283bf82870a11f54e14b85fa2c12366ae72e8d19fcbbfe7c565ad7eb64ca14e5c21c893bf22c5f6e02ab93a59fcb23cd9118bb9cb1d8511ab39fa6b497bf6cc3ff885d1d2bbcce48c949f1147954)
* [Keychron K3 Wireless Mechanical keyboard](https://www.amazon.es/Keychron-inal%C3%A1mbrico-Ultra-Delgado-Hot-swappable-Low-Profile/dp/B0BY2SCHZT/)

#### Mouse or Trackpad (135€)

* [Apple Magic trackpad](https://www.apple.com/es/shop/product/MXK93Z/A/magic-trackpad-usb%E2%80%91c-superficie-multi%E2%80%91touch-blanca?fnode=d04ee1e7f091ad6ec0d8a930b0c2a327fe25c22abfc0eb1d535c1ac9949d4decc7e0f3d7b7a951aabc5969365949eede844fc42dd07d290ea0894ad4a74cd7559447a99f3c9d5f4d7094dfc0b7d29cbb8305ae26079035c19dfe9d0b82bedbdc)
* [Apple Magic mouse](https://www.apple.com/es/shop/product/MXK53ZM/A/magic-mouse-usb%E2%80%91c-superficie-multi%E2%80%91touch-blanca?fnode=c00599bad20ff3214dd4af2cf451f231d9f48152b6cf8b6f4b5ceeed8d9b035c5b958010a5a81a2609ddbed9c5d0d5183c8168540e45f2e1fb1c2efe3760fec1f68b722e698bba56ccc27eb4c8ae94efc2e998fdb149c0729c71a7690e4acce1)
* [Logitech G402](https://www.amazon.es/Logitech-G402-botones-programables-Hyperion/dp/B00LFBEOUA/)

#### Webcam (80€)

* [Logitech C922](https://www.amazon.es/gp/product/B01L6L52K4/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8\&psc=1)

#### Headphones (30€)

* [Sony MDR-EX110AP](https://www.amazon.es/Sony-MDR-EX110AP-Auriculares-ear-micr%C3%B3fono/dp/B00I3LUUIU/ref=sr_1_13?__mk_es_ES=%C3%85M%C3%85%C5%BD%C3%95%C3%91\&dchild=1\&keywords=auriculares+sony+cable\&qid=1607328799\&sr=8-13)

### How to proceed

1. Whenever you need any material, let us know on this [Linear issue](https://linear.app/marsbased/issue/MBT-18).
2. If you choose one of the above-recommended products or any other within that budget, we'll buy the product for you, and you will receive it at home in due time.
3. If you opt for a product that exceeds the budget, please buy it yourself and send us a photo of the receipt to <admin@marsbased.com> (no invoice is needed in this case). You will get reimbursed up to the budget amount in your next payroll.

Hope your new purchase meets your expectations and allows you to work more comfortably 🙌🏼

## Language courses 🗣 <a href="#language" id="language"></a>

From MarsBased, we strongly encourage you to improve your English skills (or any other language you're interested in, such as Spanish, Japanese, or Basque 😅). You can invest up to one hour per week during the workday in English learning classes without needing to make up for the time.

### Budget

For that purpose, you get up to 180€ per quarter to spend on on-site/online lessons, platform subscriptions, or private tutors.

#### How to proceed

1. Let us know on this [Linear issue](https://linear.app/marsbased/issue/MBT-19) your preferred option for taking language lessons.
2. Make sure the academy or private professor you've chosen is able to issue us an invoice (either monthly or quarterly) so that we can make the payment to them, or we can reimburse you for the payment you previously made. The invoice should include the following billing data:

```
MarsBased SL
NIF: B66245077
Sardenya 470, 2-1
Barcelona, 08025
```

If you'd like to go for an alternative way of learning languages, let us know, and we'll do our best to adapt to it.

## Training & Learning 🤓 <a href="#training" id="training"></a>

We have different initiatives aimed at supporting Martians' professional growth and skill development. From ensuring project rotation every time when possible, to the weekly Martian Tapas session, or the participation in conferences like the EuRuKo.

### Allowance

In addition to the mentioned, MarsBased offers you an allowance of 150€ per year to invest in your learning process as a software engineer, professional, and human. This can include conference fees, course registration, coaching sessions, buying books, etc.

You may also use part of your working hours for training if:

* The training is aligned with MarsBased’s tech stack or your current role and responsibilities.
* It is discussed and agreed upon in advance with your area manager.
* It does not interfere with project delivery, client meetings, or team responsibilities.

Requests for technical training during working hours will be evaluated individually based on each person’s workload and project context. There is no need to make up for the hours used, as long as the conditions above are met.

### How to proceed

1. Whenever you want to invest in your training, let us know on this [Linear issue](https://linear.app/marsbased/issue/MBT-20) the product or service you want to get.
2. Proceed with the purchase of the product/service.
3. Send a photo of the receipt to <admin@marsbased.com>.
4. You will get reimbursed up to 150€/year in your next payroll.
5. In case the allowance isn’t used (partly or at all) throughout the year (for any reason), the remaining amount of the allowance will be added to your payroll as a "bonus" at the end of the year.

> \[!NOTE] Please note that both the unused amount and any reimbursements not issued under MarsBased’s name will be treated as part of your salary and, therefore, are subject to tax and social security retentions from your paycheck.

Have fun and enjoy the lifelong learning process! 🤹🏻‍♀️

## Referral bonus 👩🏻‍🚀 <a href="#referral" id="referral"></a>

We'd love your recommendations for colleagues you've worked with or talented people in your network who would be a great fit for our team!

Please let the People team know if you have someone in mind. Share as much of the following information as you have:

* Full name
* Role
* CV and/or LinkedIn profile
* Contact information
* How you know them
* Whether they are aware you're referring them

If you make the introduction and facilitate the conversation, you will receive a €1,000 referral bonus if we hire them. The bonus is paid after their third month with the company.

Referrals are always a great opportunity to bring on people you've loved working with in the past!

## Sports & Health 🚴🏻‍♀️ <a href="#sports" id="sports"></a>

We all know about the importance of exercising and participating in sports for our health and overall well-being, even though it often proves challenging to dedicate the time to it. From MarsBased, we'd like to give you a boost and encourage you to do the type of exercise you enjoy.

### Allowance

We offer you an allowance of 150€ per year to invest in sports and health-related activities or products. This can include gym fees, yoga lessons, the new running shoes you are willing to buy, or a session with a personal trainer. Whatever sounds appealing to you.

### How to proceed

1. Once you make your choice of the product, activity, or service you want to use, let us know on this [Linear issue](https://linear.app/marsbased/issue/MBT-20).
2. Proceed with the purchase of the product/service.
3. Send a photo of the receipt to <admin@marsbased.com>.
4. You will get reimbursed up to 150€/year in your next payroll.
5. In case the allowance isn’t used (partly or at all) throughout the year (for any reason), the remaining amount of the allowance will be added to your payroll as a "bonus" at the end of the year.

> \[!NOTE] Please note that both the unused amount and any reimbursements not issued under MarsBased’s name will be treated as part of your salary and, therefore, are subject to tax and social security retentions from your paycheck.

Have fun & stay in shape! 🏄🏻‍♀️

## Conferences 🎤 <a href="#conferences" id="conferences"></a>

Martians sometimes go to conferences as a way of learning new things and meeting other colleagues in the sector.

### Origin of proposals

In some cases, MarsBased is interested in (part of) the team participating in a specific conference, and therefore, the proposal for participation comes from the company itself. In this case, MarsBased pays for the conference fee + travel, + hotel expenses. You don’t need to recover those work hours later on.

We are also open to your proposals. If you are interested in attending a conference in the sector, please let us know by following these steps, and we will review it to see how we can help you.

### How to suggest a proposal

1. Create a new discussion in the [Linear board](https://linear.app/marsbased/team/MBT/active) for the conference you'd like to attend, and name it as "Attending + Name of the conference"
2. Include the following details about the conference:

* Event dates
* Conference fee (if any)
* How do you expect MarsBased to support you
* Why should MarsBased support you (which is the value you expect the conference will bring to you and to the company)

### Evaluation

We'll evaluate the information provided, and depending on the relevance for the company, you might get some days off for attending the conference, have conference tickets covered, or even get transportation and accommodation expenses covered. The conference will be considered:

* **Highly relevant:** when the content is directly related to your work and MarsBased activity. In this case, the company will be happy to pay for the conference fee + travel, + hotel expenses. You won’t need to recover those hours later on.
* **Interesting:** when the content is not very relevant for the company, but offers learning potential for you as a software engineer. In this case, you'll be able to attend the conference during your work schedule. You won't need to recover those hours later on.
* **Unrelated:** when the conference is not related to your work or MarsBased activity. In this case, the company offers you the flexibility to attend the conference during your work schedule and recover those hours another day as best fits you.

For conferences considered interesting and unrelated, remember you can use the allowance you get for training & learning purposes to cover the conference fee, travel, and/or hotel expenses.

You are welcome to bring your proposals! 📭

### Attending conferences: what’s expected of you

When you attend a conference on behalf of MarsBased, here’s what we expect from you.

**1) During the event**

* **Visibility.** Share at least one update on LinkedIn or X (Twitter) highlighting our presence (photo of the venue, keynote, booth, or yourself/team).
* **Photos & videos.** Capture a few photos or short clips during the event (talks, venue, team moments) and send them to Marketing so they can edit and publish on our channels.
* **Optional.** If time allows, short tweets/posts during the event are welcome.

**2) After the event**

* **Write-up (internal).** Each attendee prepares a short write-up for internal use. It doesn’t need to be polished—Marketing will refine it into a blog post.
* **Suggested formats:** “Top 5 takeaways from \[Event Name]” or “Top 3 talks I attended.”

Include highlights, key insights, and any personal reflections.

**Blog post (public).** Marketing adapts your write-up into a public-facing blog post.

**3) Why this matters**

This helps us:

* Increase visibility in the community.
* Share knowledge with our audience.
* Strengthen both personal and company reputation.

## Coworking 🏢 <a href="#coworking" id="coworking"></a>

We want everyone to have a comfortable, productive, and inspiring workspace. For most of the team, that's working from home, but we know that's not for everyone, whether due to space constraints, home distractions, or simply because you thrive in an office environment.

If working from home isn't the best fit for you, you can choose a coworking space as your alternative primary workspace. While this isn't designed as a daily hybrid supplement to working from home, we're always open to discussing exceptional situations.

### Budget

If a coworking space is your chosen main workspace, MarsBased provides an allowance of up to 150€ per month to cover or contribute to the cost.

### How to proceed

1. Let Eli know your need for coworking.
2. If you've already identified the coworking place that suits you, that's great. Otherwise, we’ll help you find one (especially if you are based in Barcelona, where we know more places).
3. Normally, coworking spaces require signing a contract, and they charge a fee on a monthly basis. Let's see what the conditions are in that specific case, and we'll agree on the easiest way to manage it (can be either directly from MarsBased or through you).
4. **Let us know beforehand** when you are going to stop using your coworking space, either **temporarily or permanently**. Let the coworking know that as well, so as to avoid being charged for that period.

Count on us whenever you need something regarding your workspace!


# Holidays, Time off & Paid leave

## Local holidays

We're a remote-working company, and the team is spread across different cities and countries.

Team members are asked to choose the local holidays that best suit them: Barcelona, Valencia, Avilés, Málaga, etc. At the beginning of the year, we set up the official holiday calendar for each team member.

Feel free to exchange any local holidays for another normal day, any time.

Use Holded to review your local holidays and days off, and to check your teammates' schedules.

## How to request days off?

We officially have 24 working days per year off, in addition to the local holidays mentioned above. All requests are managed through Holded.

* Step-by-step:
  * Log in to your Holded account.
  * Go to the 'Absences' section and select the days you want to request.
  * The People team will be automatically notified. They will review your request, and you will receive a notification once it’s approved.

> \[!TIP] For additional information, check out the [Holded User Guideline](https://drive.google.com/file/d/1FDDZT0dXfmHYCdA2P1k-fHG9Uv6IT6je/view?usp=drive_link).

Kindly bear in mind:

* Let us know well in advance if you are taking:
  * 1-2 days off, let us know at least 2 weeks in advance.
  * 3 or more days off, we will ask you to give us one month's notice.
  * More than 1 week off, let us know two months in advance.
  * You can save up to five vacation days to use in the following year. The remaining days must be taken within the current calendar year.
* Please check the viability of the project you are working on before requesting your holidays.

## Medical leaves

If you feel sick 🤒 and need to go to the doctor 👩🏻‍⚕️, there is no need for you to compensate for those hours later on. If it’s an emergency or an urgent matter, don’t worry about Holded: just inform the People team and focus on yourself. We will do it for you.

If you are sick for more than two days, we kindly ask you to send us your doctor's medical sick leave document so that we can notify the Health Authority and comply with the legal requirements. If you are sick for 1-2 days, there is no need for you to send us that document.

## Health appointment

If you have a regular appointment at the doctor or the dentist, or you need to accompany a relative to the doctor within your ordinary working schedule, you are free to organize yourself as best fits you to recover those hours.

If, for some reason, you have to visit the doctor regularly due to treatment or whatever, let the People team know, and they’ll be happy to help you find the best way to reconcile both.

## Parental leave

> \[!IMPORTANT] Congratulations to the soon-to-be parents! 👶🏻

Let us know if you’re expecting a baby, and we’ll try to support you as much as possible in that big adventure.

These are the general terms, which are discussed to best adapt to the needs in each case:

* **Pregnancy controls:** Take the time you need for the regular pregnancy controls, whether you are pregnant or need to accompany your partner. There is no need for you to recover that time afterward.
* **Parental leave:** When the baby is born, the general parental leave is 19 weeks according to the current Spanish regulations. You decide if you want to take those weeks in a row or distribute them, but at least 6 weeks must be after the birth, according to the law.
* **Breastfeeding permit:** Once you’re back from parental leave, you’ll be eligible for the breastfeeding permit until the baby is 9 months old. You can either accumulate the hours and enjoy them at once as an extension of the parental leave, or have your work schedule reduced by 1 hour per day.


# Digital disconnection policy

## 1. Purpose

At MarsBased, we care about the well-being of our team and are committed to fostering a healthy and respectful work environment. In compliance with Spanish legislation, this policy outlines the right to digital disconnection outside of working hours and adapts it to our remote and flexible organizational model.

## 2. Context

Our team works 100% remotely with flexible schedules. There is no standard schedule applied to everyone. Each person is free to organize their own working time, while ensuring they meet their individual and collective responsibilities.

## 3. Principles of the Right to Disconnect

* Everyone has the right to not respond to work-related messages, calls, or other communications outside of their working hours, even if others are working at different times.
* This right especially applies during breaks, holidays, leave, or weekends (if not part of the usual work schedule).
* Not replying outside of one's working hours will never be penalized or viewed negatively.

## 4. Best Practices

To foster a respectful and sustainable work environment:

* It is recommended to use message scheduling tools (e.g., Slack or Gmail) when working outside others’ usual hours.
* Avoid creating expectations of immediate responses outside the other person's work hours.
* Clearly inform your availability, especially if your schedule differs from the standard hours, through the appropriate channels, such as Slack and Google Calendar.
* Use Slack status updates to indicate availability, breaks, time off, or focus hours.

## 5. Exceptional Cases

Urgent communication may be justified in exceptional circumstances such as critical project issues, client emergencies, or other major unforeseen events. These situations should always be approached with good judgment and mutual respect.

## 6. Communication and Review

This policy is part of the company Handbook.

For any questions or suggestions for improvement, please contact the People team.

> \[!NOTE] This policy may be updated periodically to reflect changes within our organization or changes in applicable laws and regulations. The most recent version will always be available here, in the company Handbook.


# Travel policy

At MarsBased, we want to make trips as smooth and hassle-free as possible. That’s why our travel and expense policy is here to guide us in organizing everything clearly and efficiently. Thanks for sticking to it! If you have any questions, feel free to contact the People team 🤗

## Travel Booking Process

### How to Book Travel?

Here’s how it works:

* Get approval from your manager.
* Please contact the People team as early as possible. They will handle the booking for you. Send your request to <administration@marsbased.com>.

### Expense Categories

When planning our trips, we aim to keep things efficient and budget-friendly while meeting our needs. The earlier we plan, the better the options and savings.

#### Flights ✈️

* We travel in Economy Class.
* Changes and cancellations will only be made in exceptional cases with a justified reason.

#### Accommodation 🛏

* We usually book double rooms for a single use for extra comfort.
* Additional reimbursable accommodation costs include: Wi-Fi fees, parking fees, and breakfast charges (if not included in the room rate).

#### Ground Transportation 🚆 🚘 🚖

* Trains, subways, and buses are our go-to for getting around.
* Taxis are ok when there is no fair alternative, when the alternative takes twice as long, or when there is a justified reason.
* If you prefer to drive your car, parking fees and kilometers are also reimbursable.

#### Meals 🍝

* Per diems are fully reimbursable although we usually have our meals together and the company usually pre-paid them.

#### Non-Reimbursable Purchases 🛍️

* Clothing or toiletries.
* Airline club memberships.
* Minibar purchases or bar bills.
* Laundry or dry-cleaning services.
* Parking fines or traffic violations.
* Airline ticket change fees.
* Movies, online entertainment, or newspapers.
* Spa and health club usage.
* Loss or theft of personal belongings.
* Damage to personal vehicles.

## Expense Management and Reimbursement Processes

Send your travel expense receipts via <administration@marsbased.com>. Reimbursements will be added to your next paycheck.

## Travel Assistance and Safety

If you face missed flights, cancellations, hotel issues, or any other logistical setback. Or if you experience a medical emergency while traveling, head to a local healthcare facility and inform the People team.


# Careers at MarsBased

Everything we do, we do it thinking in the long term. We pride ourselves on the fact that we don't have employee turnover. We go for *years* without losing employees. While in other companies employees stay on average one year (or less!), at MarsBased the average is almost five years and counting!

This means that we would like to have you for years to come with us, so we can grow together.

## Career plans?

Being a small company gives us the advantage of being able to sit down with our employees on a regular basis and define their career plan with them: no arbitrary goals, no force-fed promotion to management, no bullshit.

We will check in with you regularly and define together a set of goals for you to accomplish so we can increase your salary and maybe give you a new title.

A few examples of this are:

* To work on at least two mid-sized projects in a new technology you don't know.
* To be in charge of the DevOps responsibilities in all the projects you're in.
* To be able to manage international projects successfully.
* To contribute at least once per month to the blog of the company with technical blog posts.
* Etc.

## Project rotation

We've got a lot of projects going on, some bigger than others. We try to rotate people between different projects so that they stay a maximum of 12 or 18 months in the same projects. However, because of project constraints such as the language of the client, management workload and so on, this might not always be possible.

## Roles

We are a small-sized company. We don't have a lot of roles.

We have a layer of **Management**, consisting of the three founders of the company, and Eli, our Head of People, who enables all of us to work better and makes the company run more efficiently, but that's mostly all regarding titles.

The rest of the company members are on the same level, as there is no hierarchy. All of the employee workforce have different roles in different projects, but no titles. Everyone is a software engineer.

For example, you might be a **Tech Lead** in a project, but be an **Engineer** in another, even at the same time if you're working on two projects in parallel. Likewise, you might be an Engineer in one and a Tech Lead in the next one, and that is not a promotion, it's just a role. Going from a Tech Lead to an Engineer role doesn't mean a demotion either!

That's why we don't have Tech Leads who manage software engineers, and we must be very careful to explain this to our candidates.

You, as part of the tech team, may have the following roles:

First, the **Tech Lead**, which is a very senior programmer who manages technical projects and has CTO-like abilities to weigh in into specific projects when needed. This role also gets to code, but only part-time.

Then we've got the **Senior Engineers**, which are the most common roles we're hired for. And then, naturally, the **Engineers**, who are slightly less experienced than the Senior Engineers.

Again, this is just a role. You can be a Senior Engineer in Rails for one project while being an Engineer in React for another at the same time. The roles just define what can the client expect of you and what hourly fee should we charge them.

## Salaries

Every year, we check the temperature on the market, and we revise the salaries in the company.

We like to offer good salaries which are on par with most good companies in the market. When there is a huge correction on the market, we devise a plan to schedule raises for everyone.

Every role in the company has a salary range, to give flexibility to people coming in at different times in the company.


# Software and Device Usage policy

## 1. Introduction and Scope

This policy governs the use of work tools and devices provided by MarsBased SL (hereinafter referred to as MB). It applies to the entire team, regardless of their geographic location or contractual relationship. The objective is to balance the security of company and client information with each collaborator's right to privacy, in accordance with the EU GDPR, local labor laws, and ISO 27001 security standards.

## 2. Identity and Access Management

* Google IDP: Access to all corporate tools must be managed through the Google Identity Provider (IDP) using the corporate account, where supported.
* GitHub Accounts: It is mandatory to use a separate, exclusive GitHub account for MB-related activity, following the convention mb-name-lastname. Linking corporate repositories to personal accounts is not permitted.
* Credential Management: The use of 1Password is mandatory for managing corporate passwords. Collaborators must use their individual "Employee" vault for personal work credentials, ensuring that shared "Team Vaults" contain only project-wide information.

## 3. Device Usage, Licenses, and Returns

* Company Hardware: Laptops and mobile devices are the property of MB and are provided for exclusive professional use.
* Software Licenses: Provided licenses are for work purposes only. Use for side-projects or personal activities is prohibited.
* Information Transfer: Forwarding confidential information, source code, or client data to personal accounts or unauthorized third parties is strictly prohibited.
* Remote Security: MB reserves the right to perform a remote wipe in case of theft, loss, or contract termination for security reasons.
* Hardware Return: Upon termination, equipment must be returned within 72 hours via MB's logistics service.

## 4. Privacy Commitment and Communications

* Non-Interference: MB will not access private data on Slack, 1Password, or Gmail, despite having the technical capability to do so.
* Collaborator's Privacy: To protect their own privacy and in accordance with the Confidentiality Agreement, collaborators must not upload personal photos or videos to company systems.
* Recordings: Virtual meetings may be recorded solely for operational, training, and coordination purposes, with data confidentiality ensured.
* Private Channels: Use personal tools for private life. Do not enter sensitive personal data into AI tools such as Gemini or Cursor.

## 5. Intellectual Property and Confidentiality

* Ownership: All intellectual property rights generated during the performance of duties using MB resources belong exclusively to MB.
* Confidentiality: Collaborators must handle company information with due care and maintain strict confidentiality, an obligation that continues after the professional relationship ends.

## 6. Access Exceptions and Procedure

Access to information or equipment by MB shall be strictly exceptional, justified, and proportional, limited to the following scenarios:

* Legal and Judicial Requirements: Court orders, subpoenas, or law enforcement requests.
* Asset Protection: Found suspicion of client data leaks, intellectual property theft, or activities compromising ISO 27001 certification.
* Conduct and Misuse Investigations: Evidence of harassment, discrimination, criminal activity, or misuse of company assets, devices, or intellectual property.
* Business Continuity: Recovery of critical operational information in cases of prolonged absence or termination.
* Audits: Technical access required for security audits or regulatory compliance.

## 7. Jurisdiction and Review

This policy shall be interpreted in accordance with local labor laws. This document may be reviewed and updated annually or as needed.


# Branding guidelines

## The MarsBased Branding Guidelines

When using our logos and brand identity, you've got to follow a few indications that we're listing in this section of our Handbook.

## Name

First and foremost, in written form, **MarsBased** is one word and one word only, with a capital M and a capital B. Unless required to do so, the name of the company shall not be spelt in all uppercase (MARSBASED) or all lowercase letters (marsbased).

## Logo

**Note:** If you're reading this section using Github's dark mode, you might not see things correctly. Considering turning it off temporarily.

Our main logo is the one with the name of the company:

![Main logo](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-8b415b72dd9e95425ec73c201d46ca41270bb021%2Flogo-black.png?alt=media)

Over dark backgrounds, we prefer using the red version of the logo. This is the logo used on [our website](https://marsbased.com/):

![Main logo over dark backgrounds](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-c88b6daa6c0d66842087ab3d131fc2328d23bd22%2Flogo-red.png?alt=media)

We have an alternative white version of the logo, but we don't use it very often nowadays:

![Alternative logo over dark backgrounds](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-4eb14a64efd5339001abfdaf40db9d79cf4a009a%2Flogo-white.png?alt=media)

Finally, we sometimes use green/turquoise when the red color is already highly prominent, or used by a different title, like on our [podcast website](https://podcast.marsbased.com/):

![Alternative logo](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-b92f74da11d540ac4ef740698dc47b832c297b26%2Flogo-green.png?alt=media)

Only use the green/turquoise logo over a #262728 background, like in this example:

![Alternative logo example](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-9c3882e6bfe419211c36118245f9fe90c999bd0f%2Flogo-green-example.png?alt=media)

## Icon / Abbreviated

Sometimes, when we're forced to use a smaller logo or one with squared proportions, we use the following ones instead:

![Main abbreviated logo](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-f372913f56d451bce80b2c72252a3b10904d9bc6%2Flogo-squared-black.png?alt=media)![Main abbreviated logo over dark backgrounds](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-57b5a78da1e9fae61ba4265ab1aae198a4a679f9%2Flogo-squared-red.png?alt=media)![Alternative abbreviated logo over dark backgrounds](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-119d4f04421e45f706a94bfdfbd2e8712ae70561%2Flogo-squared-white.png?alt=media)![Alternative abbreviated logo](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-edd7c97666646d69aa6584caa22cae574d3e4d9a%2Flogo-squared-green.png?alt=media)

Follow the same color principles explained above.

## Social profile picture

In social networks, platforms, and tools, where we need to display a company profile picture, we typically use one of the following options:

![Social picture](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-38ff445b46d770f36a6cc712ccf05231ba1c1b4c%2Flogo-social-red.png?alt=media)![Alternative social picture](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-ea062bd704ca48823920992a5e0df2710d14abf3%2Flogo-social-green.png?alt=media)

## Corporate colours

We use the following colours in all of our communications and assets:

**Red Mars ©**

* RGB: 255, 0, 51
* CMYK: 0%, 100%, 80%, 0%
* HEX: #FF0031

**Green Martian ©**

* RGB: 0, 255, 191
* CMYK: 100%, 0%, 25%, 0%
* HEX: #00FFBE

**Grey Space Suit ©**

* RGB: 38, 39, 40
* CMYK: 5%, 2%, 0%, 84%
* HEX: #262728


# Project management guidelines

This document contains information on actions related to managing projects that should be shared across all projects.

That way, software engineers and tech leads will work the same way regarding management, no matter which project they are working on. It will also help project managers when we need to transfer a project or check the status of another project for whatever reason.

## Daily updates

Set a workflow on the internal Slack channel of the project at 12 pm each weekday, so software engineers can share the progress they have made since the last update they did. This helps software engineers to share their progress, comment on blockers, doubts, etc. Take into account that sharing this information does not excuse them from maintaining Linear up-to-date.

![Daily Status Update Workflow on Slack](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-21944bf8ae0ce16d8b67e431aa51b7c831a5b665%2Fdaily-status.png?alt=media)

> **NOTE:** To create a workflow on the channel, you can just copy a similar existing one and make the needed adjustments to fine-tune it to the project.

## Internal weekly reports

Internal weekly reports have the goal to help project managers fill the progress reports that are sent to the client. They are supposed to be filled by everyone working on the project on Friday, before they close their week. Filling it on Friday helps software engineers to have all the issues more recent in their heads. That way, project managers have all the information ready on Monday to fill the progress report for the clients.

> **NOTE:** Although team members fill these reports, it is still good to check the Linear status of the project to make sure nothing is left out of the report.

The template for the Internal Weekly Report can be found here: [Internal Weekly Report](https://docs.google.com/document/d/1BRmL2qPBwbF6WfapEaoOjqvPqsfS0Y9ijem9OS6GT4A/edit?usp=drive_link). It is already available as a Google Doc template in the MarsBased workspace, so when creating the first internal weekly report for your project you can do it with the option “From a template” when creating a new Google Document. Please, create the report on Monday so team members can fill it up during the week if they consider it appropriate, or just in case someone closes earlier than Friday.

To make sure everyone remembers to fill the report, we have 2 reminders:

* On Friday, we tag each person in a comment on the document.
* On Slack, we have a weekly reminder using also a workflow (on Friday, at 9 am).

![Internal Weekly Report Workflow on Slack](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-ad1860466668db75082b7e1b443e1f5a4f2f4b17%2Fweekly-report.png?alt=media)

> **NOTE:** To create a workflow on the channel, you can just copy a similar existing one and make the needed adjustments to fine-tune it to the project.

## Budget control

On projects that are closed-scope and we need to keep on a budget, it is good to do budget control on a weekly basis. Preferably, this should be done at the beginning of the week, to make sure Harvest is up to date.

To standardise this, we can use the same organization on a spreadsheet and keep improving it as we see the need.

An example of this document can be found in the Project Management Templates Drive folder, in the file [Hours control example](https://docs.google.com/spreadsheets/d/118kwlli8-m5qEXpIBtwdkWlEV9q54Thu431lUPG-0QU/edit?usp=drive_link). If you have any doubts regarding the content of this file or how to use it, contact another project manager.

## Drive folder organization

1. **File naming.** Include in the name of the files the date using the following format: `<YYYY-MM-DD> <Name of the file>`, especially those of meetings and reports. That way, documents can be easily sorted, and the last ones can be accessed.
2. **Folder structure.** Try to adhere as much as possible to the following structure:
   * **Reports.**
     * Folder that contains all reports sent to the client.
   * **Internal weekly reports.**
     * Folder that contains all the internal weekly reports of the team.
   * **Meeting notes.**
     * Folder that contains all the notes of the meetings of the project.
     * Ideally, there should exist notes for all the weekly internal project review meetings and all meetings with the clients that have some entity (progress meetings, change of scope meetings, etc.).
   * **Functional documentation.**
     * Folder that contains all the functional documentation of the project.
   * **Technical documentation.**
     * Folder that contains all the technical documentation of the project.
   * **Contracts.**
     * Folder that contains all the contracts signed with the client.
   * **Archive.**
     * Folder that contains old documents of the project, which are outdated or not relevant anymore, but are maintained there for traceability purposes. E.g., functional definitions or DB schemas that are outdated.
   * **Shared.**
     * Folder that contains documents shared with the client.

> **NOTE:** All folders can be organized internally by years if necessary.

## Use of Google Gemini

To ensure effective communication and proper documentation of meetings and updates, the following steps should be followed:

1. **Record weekly internal meetings**\
   It is important to record internal weekly meetings to maintain a record of discussions and decisions made.
2. **Record weekly meetings with the client / relevant meetings**\
   All weekly meetings with the client, as well as other relevant meetings, should be recorded for future reference and to ensure that all points of discussion are documented.
3. **Move to the project’s meeting notes folder**\
   After recording the sessions, the recordings should be organized and transferred to the “meeting notes” folder of the respective project in Google Drive. This will facilitate access and future consultation.

   ![Gemini Settings](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-31b992eae5d62de69802e4bd277c1313860cf9a3%2Fgemini-settings.png?alt=media)
4. **Set up to ensure everyone receives the notes**\
   Make sure that all team members have access to the meeting notes (and recordings when necessary) and receive appropriate notifications to avoid missing any relevant information.


# Linear guidelines

This document contains information and templates about how to use Linear for organizing projects and defining issues.

## Table of Contents

* [Milestones](#milestones)
  * [Use Milestones to Track the Progress of a Project](#use-milestones-to-track-the-progress-of-a-project)
  * [Assign Tasks to Milestones](#assign-tasks-to-milestones)
* [Epics](#epics)
  * [When to use Epics](#when-to-use-epics)
  * [How to Use the Epic Tag](#how-to-use-the-epic-tag)
* [Issue Title](#issue-title)
  * [Be Descriptive and Concise](#be-descriptive-and-concise)
  * [Include Page/Module Name in the Title](#include-pagemodule-name-in-the-title)
* [Labels for Issues](#labels-for-issues)
  * [Purpose of Labels](#purpose-of-labels)
  * [Recommended Labels](#recommended-labels)
* [Project & Issues Linear Templates](#project--issues-linear-templates)
  * [Project Template](#project-template)
  * [Bug](#bug)
  * [Independent Frontend Task](#independent-frontend-task)
  * [Independent Backend Task](#independent-backend-task)
  * [Independent Task with Frontend and Backend Work](#independent-task-with-frontend-and-backend-work)
  * [Epic](#epic)

## Milestones

### Use Milestones to Track the Progress of a Project

Break down projects into key milestones. Each milestone should represent a significant achievement or phase in the project. The definition of these milestones will depend on the specifics of the project, and it is essential to analyze and determine, at the beginning, how the project should be distributed across its phases. Each milestone should correlate with a tangible deliverable of the project. They can represent various elements such as:

* **Modules of the Application.** Key components or features that define the project scope.
* **Important Dates.** Significant deadlines throughout the project lifecycle, such as design phase completion, when the project will be ready for Quality Assurance (QA), or the date scheduled for the production release.

### Assign Tasks to Milestones

Link Linear issues to the relevant milestone. This helps in tracking progress and ensures that all tasks contribute to the project’s objectives.

## Epics

### When to use Epics

The project manager should use the Epic tag in the following scenarios:

* **Defining Major Features.** When a client request or project requirement involves a substantial feature that requires multiple tasks or components to be developed.
* **Decomposing Complex Tasks.** When identifying complex tasks that exceed a single sprint or iteration timeline, and thus it needs further breakdown into smaller, actionable items.
* **Cross-Functional Initiatives.** When a project spans multiple teams or disciplines (e.g., frontend, backend, QA), and it requires coordination among various contributors.

### How to Use the Epic Tag

1. **Create an Epic Issue.** In Linear, create a new issue representing the Epic and apply the "Epic" tag. Provide a clear and concise title followed by a detailed description of the feature's scope, objectives, and any relevant criteria for completion.
2. **Assign the Epic to the PM.** Assign this Epic issue to the Project Manager who will oversee its development.
3. **Decompose into Subissues.** Break the Epic down into smaller subissues that specify individual tasks or components necessary to achieve the Epic's objectives. Each subissue should include:
   * A clear title and description.
   * Assignment to the appropriate software engineer or team member responsible for executing that task.
4. **Link Subissues to the Epic.** Ensure that all relevant subissues are linked back to the Epic issue. This is only necessary if the subissues are NOT created using the **+ Add sub-issues** action of the Epic task.

![Subissues Creation from Existing Issue](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-2bfd1a8e8e81a6de969ce20b22dd86dd775af5ef%2Fsub-issues-creation.png?alt=media)

5. **Close the Epic Upon Completion.** Once all subissues are completed and the feature is fully developed and tested, the PM should mark the Epic as complete.

## Issue Title

### Be Descriptive and Concise

Use clear and descriptive titles that accurately convey the essence of the task or issue. Aim to keep the title concise, yet informative enough to understand the context without additional detail.

### Include Page/Module Name in the Title

When needed, clearly specify the page or module where the issue or feature will be applied. This helps to contextualize the task and allows for easy identification of where changes or developments will take place. Always use the following format for including the page or module name in the issue title:

* `[<Module/Page>] <Brief Description>`
* *Example:* \[Login Page] Fix UI Overlap Issue

## Labels for Issues

### Purpose of Labels

Labels in Linear serve to categorize and provide context for issues, making it easier for team members to filter and prioritize tasks.

### Recommended Labels

* ***Technology*** **Labels.** Especially useful in projects that have a backend and a frontend, or that mix several technologies. MarsBased already has a set of labels that are specific to technologies in the first label. If you think you might need a different one, ask another PM to help you on this.

  ![Technology Labels](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-33f163d4d0337a36a7173a1e7ba02922cf8e06e2%2Ftechnology-labels.png?alt=media)
* ***Type*** **Labels.** This label is essential for categorizing issues based on their nature or activity within the project. As for technologies, MarsBased already has a set of labels that are specific to types.

  ![Type Labels](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-a1295b75feaac24a9a53c7e4746c22cb0598a0d5%2Ftype-labels.png?alt=media)
* ***Scope*** **Labels.** This label serves to classify issues based on their relationship to the project's established scope. It helps teams track changes in features and functionalities, ensuring everyone understands whether an issue pertains to new developments, adjustments, or exclusions from the original project scope. Possible values:
  * **New.** This label indicates that the issue pertains to a new feature or requirement that was not included in the original project scope or initial estimates.
  * **Out of Scope.** This label is used for features or tasks that are deemed outside the project's defined boundaries and will not be included in the current development cycle, but we still want to keep them in the project for traceability purposes.
  * **Updated.** This label signifies that there have been changes to previously defined features or requirements during the project lifecycle that affect the original scope of the task and its estimates.

    ![Scope Labels](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-ef989b9f66d0c36768b906c52101a9fa4eb82ebb%2Fscope-labels.png?alt=media)
* ***Role*** **Labels.** This label serves to classify issues based on the role that should develop it.

  ![Role Labels](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-8609967239fa83eea74904297a26992a8182dfea%2Frole-labels.png?alt=media)

> **NOTE:** If at some point you think you might need a different label for any category, ask another PM to help you with this.

## Project & Issues Linear Templates

All these templates are available in Linear too.

### Project Template

Corresponds to the template of the Overview page of the projects in Linear.

* **Description.** Short description of the project.
* **Architecture.** Overview of the architecture of the project.
* **Who is who.** This section provides a brief introduction to the key team members involved in the project, outlining their roles and responsibilities.
* **Deployments.** Explanation of how deployments are done in the project. Differentiate between Staging and Production deployments if needed.
  * **Who can deploy.** List of people authorized to perform deploys of the project. Differentiate between Staging and Production deployments if needed.

### Bug

* **Label.** Use always *Bug*.
* **Environment.** Specific environment details (e.g., browser, OS).
* **Steps to Reproduce.** Instructions to replicate the issue.
* **Actual Outcome.** What actually happened.
* **Attachments.** Videos or images supporting the actual outcome.
* **Expected Outcome.** What the expected behavior should be.

### Independent Frontend Task

* **Description.** Detailed explanation of the task requirements.
* **Error Control.** Errors that need to be handled.
* **Acceptance Criteria.** Conditions that must be met for the task to be considered complete.
* **Design Specifications.** Relevant design files or mockups for the task.
* **Modifications Over Design.** Changes over the previous files that need to be implemented.
* **Adherence to Design.** Is pixel-perfect needed?
* **Performance Considerations.** Notes on performance or optimization requirements.
* **Component Name.** Name of the component or element being created.
* **Other Considerations.** Cross-browser compatibility, accessibility, etc.

### Independent Backend Task

* **Description.** Detailed explanation of the task requirements
* **Error Control.** Errors that need to be handled
* **Acceptance Criteria.** Conditions that must be met for the task to be considered complete
* **Technical Definition.** This may include:
  * **Source Code Details.** Guides on how the feature should be implemented and/or parts of the source code that need to be modified or taken into account.
  * **Database Changes.** Specific changes needed in the database (e.g., schema modifications)
  * **API Endpoints.** New or updated API endpoints to be implemented
  * **Security Considerations.** Notes on required security measures or compliance
  * **Testing Requirements.** Details on testing that needs to be performed (e.g., unit tests, integration tests)

### Independent Task with Frontend and Backend Work

* **Description.** Detailed explanation of the feature requirements.
* **Error Control.** Errors that need to be handled.
* **Acceptance Criteria.** Conditions that must be met for the feature to be considered complete.
* **Attachments.** Relevant files, mockups, design specifications.
* **Frontend Tasks.** Specific tasks or implications to be developed in the frontend.
  * **Design Specifications.** Relevant design files or mockups for the task.
  * **Modifications Over Design.** Changes over the previous files that need to be implemented.
  * **Adherence to Design.** Is pixel-perfect needed?
  * **Performance Considerations.** Notes on performance or optimization requirements.
  * **Component Name.** Name of the component or element being created.
  * **Other Considerations.** Cross-browser compatibility, accessibility, etc.
* **Backend Tasks.** Specific tasks or APIs to be developed on the backend.
  * **Technical Definition.** This may include:
    * **Source Code Details.** Guides on how the feature should be implemented and/or parts of the source code that need to be modified or taken into account.
    * **Database Changes.** Specific changes needed in the database (e.g., schema modifications).
    * **API Endpoints.** New or updated API endpoints to be implemented.
    * **Security Considerations.** Notes on required security measures or compliance.
    * **Testing Requirements.** Details on testing that needs to be performed (e.g., unit tests, integration tests).

### Epic

* **Label.** Use always *Epic*.
* **Assignee.** Use always *\<project\_manager>*.
* **Description.** Detailed explanation of the feature requirements.
* **Attachments.** Relevant files, mockups, design specifications.
* **Frontend Tasks (as subissues).** Specific subtasks to be developed in the frontend (created as actual subissues in Linear).
* **Backend Tasks (as subissues).** Specific tasks or APIs to be developed on the backend (created as actual subissues in Linear).


# Calendar, Slack & Linear working hours and status guidelines

Defining working hours and availability for each team member across the various tools we use is essential for managing expectations and facilitating effective scheduling of meetings and tasks. You can contribute to this aspect in several ways:

* [Set your working hours in Google Calendar](#set-your-working-hours-in-google-calendar)
* [Set your out-of-office hours in Google Calendar](#set-your-out-of-office-hours-in-google-calendar)
* [Connect Google Calendar with Slack](#connect-google-calendar-with-slack)
* [Connect Google Calendar with Linear](#connect-google-calendar-with-linear)
* [Check your coworkers' availability](#check-your-coworkers-availability)

> **IMPORTANT:** We only need information about your usual daily schedule. If you need to make a change to your schedule because of a doctor’s appointment, for instance, your working hours and out-of-office periods do not need to reflect that.

## Set Your Working Hours in Google Calendar

Make sure your regular working hours are updated in your Google Calendar to facilitate effective meeting scheduling and improve overall team coordination.

You can define your usual working hours in Google Calendar by following these steps:

1. *Access Calendar settings*

   ![Calendar Settings](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-1b8c551b698f4bd31afa26e099c4178f6060c163%2Fcalendar-settings.png?alt=media)
2. *Define your working hours*
3. In *General → Working hours*, tick the checkbox *Enable Working Hours*.
4. Select the days of the week you work.
5. Enter your working hours.
   * If you work the same schedule every day, define it for the first day and then click *Copy to all*. ![Calendar Working Hours Settings](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-71842d6aa0378e1a6dbd541d44b38f1f69deb965%2Fcalendar-wh-settings.png?alt=media)

📘 **Official documentation:** [Google Calendar Working Hours Help](https://support.google.com/calendar/answer/7638168?hl=en\&co=GENIE.Platform%3DDesktop)

## Set Your Out-of-Office Hours in Google Calendar

To automate setting your status to *Out of Office* in both Slack and Linear, you need to tell Google Calendar when you are out of office.

The Out of Office events need to complement the working hours you set up in the previous step.

When you are done setting your working hours and out-of-office events, your calendar should look something like this:

* Working hours will be shown with a line of light color.
* Events in light colors will represent out-of-office hours.
* Working hours and out-of-office events complement each other and do not overlap.

  ![Calendar with Working Hours and Out of Office configured](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-c582c3d91bd7b19acc4c0bfe90740acd1f876943%2Fcalendar-with-ooo.png?alt=media)

### To Create Out-of-Office Events

1. Open Google Calendar.
2. Create an event and select *Out of office* from the event type dropdown.

   ![Creating an out of office event](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-b683a9444741422aadd077f11038c82f0c41d463%2Fooo-creation.png?alt=media)
3. Set the start and end time for your out-of-office period.
4. Set the *Repeat* option to *Weekly*, so the same out-of-office events are copied to all weeks.
5. (Optional) Enable *Automatically decline meetings* to prevent scheduling conflicts.
6. Repeat steps 2–5 until you define all the timeframes you are not working.

## Connect Google Calendar with Slack

Integrate Google Calendar with Slack to receive notifications about your upcoming meetings and stay updated on any schedule changes. In addition, your status will automatically change when you are in meetings, out of the office, etc.

📘 **Refer to the official documentation to connect your account and calendars to Slack:** [Google Calendar for Slack](https://slack.com/help/articles/206329808-Google-Calendar-for-Slack#:~:text=to%20your%20workspace.-,Connect%20your%20account%20and%20calendars,-Connect%20your%20Google)

## Connect Google Calendar with Linear

Similar to Slack, integrating Google Calendar with Linear will automatically update your Linear status to *Out of Office*. To connect your Google Calendar to Linear, follow these steps:

1. **Access Linear Settings.** On the Linear website, click the MarsBased workspace icon and select *Settings*.

   ![Linear Settings](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-55a133c5e5a5131ea2dd3dfcefafc88419fd0bb9%2Flinear-settings.png?alt=media)
2. **Connect Google Calendar.** Go to the *Connected Accounts* section, locate *Google Calendar*, and click *Connect*.

   ![Linear Connect Accounts Settings](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-ee0c4a3b1f9997384e9c5b6cf5a473fce62080b5%2Flinear-connected-accounts-settings.png?alt=media)
3. **Complete the Connection.** Follow the on-screen prompts to finish linking your Google Calendar to Linear.

## Check Your Coworkers’ Availability

Once all MarsBased team members have completed the setups above, you can expect the following benefits:

1. **Enhanced Availability Information.** To view a colleague’s daily schedule, use the *Meet with…* option in Google Calendar. *Tip: Uncheck your main calendar for easier viewing.*

   ![Calendar \<i>Meet With...\</i> option](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-c1cc2486f27a52c76801e36b7b31f8df2dd341a7%2Fcalendar-meet-with.png?alt=media)
2. **Accurate Status Updates.** Both Slack and Linear will automatically show when a team member is *In a meeting* or *Out of Office*.
3. **Better Google Calendar Event Planning.** When scheduling an event and inviting a MarsBased team member, Google Calendar will indicate with a *moon icon* 🌙 if the event is planned outside of their office hours.

   ![Planning a meet with Calendat](https://1356411749-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FHefFyj5BgaNQtsjDpcpm%2Fuploads%2Fgit-blob-1fd00d574da2bc4757f79ebeaa7f8e07dc58a50f%2Fcalendar-planning-meet.png?alt=media)


# File storage, permissions & security

## 1Password

We use 1Password as our tool of choice to manage permissions and logins to all the environments, tools and other confidential information we need for our projects.

Everyone has access to the basic logins that should be available company-wide because we organise them in vaults.

Every project has got a vault to which you will have access if you are working on that project.

We ask everyone to keep the logins tidy and up-to-date and the passwords as strong as possible. 1Password can help you create strong and secure passwords. So remember that if you have signed up for a new tool or created a login that someone else might use, you should add it to the project's vault accordingly.

When you're relocated to another project, your access to the former project's vault will be revoked to avoid leaving stuff floating around.

## File storage and file-sharing guidelines

Here you will find our guidelines to use Google Drive, our file storage tool of choice.

### Where should we store the documents?

All the documents of the company must be stored in our Google Drive's shared drive called "MarsBased". Google Drive is our central file storage solution.

The folders distribution hasn't changed that much:

* We still have one folder for each client or project (under *Projects*);
* a folder where we store the document templates (*Templates*);
* *Sales* and *Marsketing* folders;
* *Martian Tapas*, where you can find the Martian Tapas recordings;
* a *Design* folder with the company logos and images;
* *Organization*, which contains the Forecast & Time Off spreadsheet, among other documents;
* *Team* includes a short biography of each one of your Martian colleagues (some of them are very funny!);
* *Guides*, with books, guides, and training resources;
* and finally, *Martian Days*, with company presentations.

### Sharing policy

Sharing files in Google Drive is easy but then very difficult to manage. Google Drive doesn't have an option to see which files have been shared outside the company easily. For that reason, we need to think twice before sharing something with someone, freelance, provider or client.

For security reasons, we don't want people from outside the company having access to our folders and documents.

* Never share an entire folder with someone, unless absolutely necessary. If you have to do it, for any reason, rename the folder adding (shared) at the end to make it easier to see that it has been shared outside MarsBased.
* Never share anything only with a public link, unless absolutely necessary. It's much better to share with individual people.
* If the people you want to add don't need editing capabilities, make sure to add them as "viewers" or "commenters".
* If you add someone as a viewer or commenter, make sure to set an expiration date. Google Drive allows you to define expiration dates up to a year when you share a file with one of these roles. If that person needs access after the expiration date, she/he can make a request again. Sadly, editors can't have an expiration date.
* If you have doubts or questions before sharing something with someone, ask Jordi and he will clarify.

### Naming

We always add the date before a file name to be able to see when was the file created easily. Use the following format: YYYY-MM-DD. We only skip the date in files that are constantly edited and therefore are always up to date or files where the date is completely irrelevant. Some examples are a guide, a Ruby test or a template.


# Prompt engineering basics

## 🎯 Goal

Learn how to write better prompts to get the most out of ChatGPT and similar tools — for *all* kinds of tasks, not just coding.

***

## 🚀 Why This Matters

Most people write prompts like this:

> “Explain X”\
> “Write code for Y”\
> “What’s a good name for a feature?”

That’s the equivalent of Googling "stuff".

Let’s go pro.

***

## 📦 Core Concepts of Prompting

### 1. **Context is King**

LLMs love context. The more you give, the better the output.

> ❌ “Write a blog post about AI.”\
> ✅ “Write a 500-word blog post about how small dev agencies like MarsBased can use AI to automate documentation. Audience: CTOs. Tone: witty but professional.”

> ✅ “You're a senior backend dev at MarsBased. Given this GitHub issue, propose 3 implementation options with tradeoffs.”

***

### 2. **Role Assignment**

> “You're a senior full-stack dev.”\
> “Act like a sassy but accurate technical recruiter.”\
> “You're a product manager writing internal release notes.”

Roles change everything. Try it.

***

### 3. **Examples > Explanations**

Few-shot prompting is powerful.

> ❌ “Write me an error message.”\
> ✅ “Here are 2 examples of error messages from our system:\n\n1. 'We couldn’t connect to GitHub. Check your token.'\n2. 'Your session expired. Please log in again.'\n\nNow write a message for a failed API sync with Linear.”

***

### 4. **Constraints Help**

> “Explain this like I’m five.”\
> “Summarize this in 3 bullet points.”\
> “Reply in markdown, use emojis.”\
> “Write code with comments in Spanish.”

Constraints = better outputs.

***

### 5. **Iterate, Don’t Expect Magic**

You’re allowed to keep going:

> “That’s too long. Can you shorten it?”\
> “Add more sarcasm.”\
> “Use TypeScript instead.”\
> “Rewrite using a more optimistic tone.”

LLMs love feedback.

***

## 🔧 Dev Use Cases

* 🔍 Debugging:\
  Paste an error and ask for causes + fixes.
* 📄 PR Summaries:\
  “Summarize this PR in 3 sentences for a non-dev.”
* 🧠 Naming things:\
  “Suggest 10 function names. Context:…”
* 📜 Commit messages:\
  “Write a conventional commit for this diff.”
* 🤖 GitHub Copilot prompts:\
  Use natural language to write comments that guide code generation.

***

## 💼 Non-Dev Use Cases

* ✍️ Docs:\
  “Turn this bullet list into internal documentation.”\
  “Write a changelog based on these commits.”
* 📣 Marketing:\
  “Write a tweet thread from this blog post.”\
  “Summarize this podcast transcript for LinkedIn.”
* 📅 Planning:\
  “Create a rough roadmap based on these 4 Linear issues.”
* 🧑‍🏫 Hiring:\
  “Generate 5 technical interview questions for a senior Vue dev.”

***

## ⚙️ Bonus: API-Level Prompting (1 Minute)

If using OpenAI's API:

```json
{
  "model": "gpt-4o",
  "temperature": 0.7,
  "messages": [
    { "role": "system", "content": "You are a senior Rails engineer." },
    { "role": "user", "content": "Explain the pros and cons of Hotwire." }
  ]
}
```

* **system**: Sets the role/persona
* **user**: The actual prompt
* **temperature**: 0 = deterministic, 1 = creative/random

## 🎁 Cheat Code: Prompt Structure

* \[Persona or role]
* \[Clear task]
* \[Context]
* \[Constraints]
* \[Optional: examples]
* \[Optional: output formatting]

🔥


# Our SEO guidelines for new projects

As a development consultancy, we get the chance to work on very different products and projects. From all of them, we learn something new, but in all of them, we apply our previous knowledge.

With dozens of different projects in well over five years, we've compiled our SEO 101 that we apply to all our projects.

***

## Tools

### Google Search Console

**URL:** <https://search.google.com/search-console>

**Description:** Google Search Console is a powerful tool for managing your domains and monitoring their performance in Google search. From domain verification to keyword analysis and error tracking, this is an indispensable tool.

* Use the **domain property** verification method to automatically include all variants of your domain (e.g., http/https, www/non-www).
* Key features:
  * **Core Web Vitals** report to monitor performance metrics: Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS). INP replaced First Input Delay (FID) as the responsiveness metric in 2024 — if you find older guides or tooling referencing FID, treat it as deprecated.
  * **Page Indexing** report (under *Indexing > Pages*) to track and resolve indexing issues.
  * **Generative AI performance** report to see how your content shows up in Google's AI-powered search features (AI Overviews, AI Mode).

### Google Analytics 4 (GA4)

**URL:** <https://analytics.google.com>

**Description:** GA4 is Google's event-based analytics platform, with privacy-centric data collection and predictive insights.

* Obtain the GA4 tracking code from the Admin section and embed it in the `<head>` of your HTML (or load it via a tag manager).
* GA4 supports custom event tracking and enhanced measurement for scrolls, video engagement, and more.
* If the project targets the EU/UK, configure **Consent Mode** so tracking respects the visitor's cookie consent choice.

***

## Sitemap

The sitemap is typically an XML file that describes the structure of your site and is placed at the root of the project.

* Use dynamic sitemaps generated by CMSs or frameworks to ensure they stay up-to-date with site changes.
* Submit your sitemap URL via Google Search Console and Bing Webmaster Tools.

Example dynamic sitemap URL: <https://marsbased.com/sitemap.xml>.

***

## Robots.txt

Robots.txt guides crawlers on how to interact with your site. It is located at `<www.site.com/robots.txt>`.

* Modern best practices:
  * Allow access to important resources like JavaScript and CSS for proper rendering and indexing.
  * Example configuration:

```plaintext
User-agent: *
Disallow:
Sitemap: https://www.marsbased.com/sitemap.xml
```

Test and submit your robots.txt file using Google Search Console.

***

## HTTPS

Ensure HTTPS is enforced site-wide, as non-HTTPS sites are flagged as insecure and perform poorly in rankings.

* Use at least **HTTP/2**; prefer **HTTP/3** (QUIC) where your hosting/CDN supports it, since it's now widely adopted and improves load times, especially on mobile networks.
* Implement **HSTS (HTTP Strict Transport Security)** for additional security.

***

## Tags, Metatags, and Schema.org

### Metatags

Modern SEO no longer uses outdated tags like `author` or `copyright`. Instead, include:

```html
<meta name="description" content="MarsBased is a web & mobile development consultancy.">
<meta property="og:title" content="MarsBased | Web Development">
<meta property="og:description" content="Development consultancy for web & mobile applications.">
<meta property="og:image" content="https://www.marsbased.com/og-image.png">
```

`index, follow` is the default behavior for `<meta name="robots">`, so you only need to add it explicitly when overriding it (e.g. `noindex` on a staging page).

### Schema.org

Use **JSON-LD** for structured data, as it's preferred by search engines:

```json
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "MarsBased",
  "url": "https://marsbased.com",
  "sameAs": [
    "https://x.com/marsbased",
    "https://www.linkedin.com/company/marsbased"
  ]
}
</script>
```

Validate with [Google’s Rich Results Test](https://search.google.com/test/rich-results).

***

## Content

Key guidelines for content creation:

* Use **E-E-A-T** principles: demonstrate Experience, Expertise, Authority, and Trustworthiness.
* Internal linking: ensure every page is linked to another for full crawlability.
* Avoid duplicate content. For necessary duplicates (e.g., multi-language), use canonical tags:

```html
<link rel="canonical" href="https://www.site.com/original-page" />
```

* Optimize headers:
  * Only one `<h1>` per page.
  * Maintain hierarchical structure (e.g., `<h2>` under `<h1>`, `<h3>` under `<h2>`).

***

## Bonus Track #1: AI Search (AEO/GEO)

"AI search" actually covers two different readers, and they don't take the same input:

* **Google's AI Overviews and AI Mode** are built on Google's normal Search ranking and quality systems. Per Google's own guidance, there's no special file or markup needed to appear in them — no `llms.txt`, no chunking content for AI consumption. The classic SEO fundamentals in this guide (E-E-A-T, structured data, genuinely helpful content) are what feed them. See [Google's AI features documentation](https://developers.google.com/search/docs/appearance/ai-features).
* **Conversational answer engines** (ChatGPT, Claude, Perplexity) work differently: they fetch and cite pages directly, so serving them a clean, dedicated version of your content (e.g. `llms.txt`, per-page Markdown) measurably helps you get read and cited correctly. This is GEO (Generative Engine Optimization) proper, and it's what we cover in our [GEO guidelines](/our-guides/geo-guidelines), distilled from the AI-discoverability work shipped on marsbased.com itself.

In short: don't bother with `llms.txt` for Google's AI Overviews, but do bother with it — along with the rest of the GEO guidelines — for being cited by LLM-based answer engines.

***

## Bonus Track #2: Migrations

For URL structure or domain migrations:

* Use 301 redirects to preserve SEO value. Example:

```plaintext
Redirect 301 /old-page /new-page
```

* Audit the migration using tools like [Screaming Frog SEO Spider](https://www.screamingfrog.co.uk/seo-spider/).

***

## Bonus Track #3: Multi-language Sites

Use hreflang tags to signal equivalent content in different languages:

```html
<link rel="alternate" hreflang="en" href="https://www.site.com/en/" />
<link rel="alternate" hreflang="es" href="https://www.site.com/es/" />
```

Validate hreflang implementations using tools like [Ahrefs](https://ahrefs.com/) or [Semrush](https://www.semrush.com/).

***

By following these updated guidelines, you'll ensure your projects are SEO-optimized and aligned with modern best practices.


# Our GEO guidelines for new projects

GEO (Generative Engine Optimization) is the practice of making a site easy for LLM-based systems (ChatGPT, Claude, Perplexity, Google AI Overviews) to read, quote, and cite correctly. It's SEO's younger sibling: same goal (be found and trusted), different reader (a model summarizing your page instead of a crawler ranking it).

These guidelines are distilled from the AI-discoverability work we shipped on marsbased.com itself. The site's own codebase (internally worked on as `mbweb`, and named `Perseverance` on GitHub) is where we built and hardened every pattern below (mainly Linear tickets WEB-513, WEB-574, WEB-577, WEB-579 and WEB-588). We treat our own site as the reference implementation: everything below is running in production, not theory.

***

## Principles

1. **Give LLMs a clean, dedicated version of your content.** Don't make a model scrape and guess at your HTML. Offer plain Markdown as a first-class citizen.
2. **Only expose what's worth citing.** Not every URL deserves a Markdown twin: ephemeral, list-only, or messily-sourced pages should be excluded rather than degrade the average.
3. **An unauthenticated content-rendering endpoint is an attack surface.** Treat it with the same care as any other public endpoint: bound its cost, cache it, and don't let it become a DoS or content-sniffing vector.
4. **Don't block AI crawlers by default.** Being cited by an answer engine is closer to earned media than to a scraper stealing your content; allow it unless a client has an explicit reason not to.
5. **Structure content the way a model would want to quote it.** Clear headings, direct answers, FAQ-style Q\&A, and schema.org markup all make extraction easier and more accurate (see our [schema.org guidelines](/our-development-guides/schema)).

***

## `llms.txt` and `llms-full.txt`

[llms.txt](https://llmstxt.org) is the emerging convention for a machine-readable "map" of your site, similar in spirit to `sitemap.xml` but written in prose for a model instead of markup for a crawler. We serve it at the root and mirror it per locale.

* **`llms.txt`**: short, a one-paragraph summary of who you are, followed by a curated, categorized list of your most important pages with one-line descriptions. Written in the company's real voice, not boilerplate.
* **`llms-full.txt`**: the long-form companion, a fuller written profile (methodology, differentiators, how you work) for a model that wants more context than the link list gives it.
* **Mirror both per locale** (e.g. `/es/llms.txt`, `/es/llms-full.txt`) if the site is multilingual.
* **Point every page entry at its Markdown version**, not the HTML page, using a documented, predictable convention (see below).
* Keep both files curated by hand. This is editorial surface, not a generated dump; treat it like you'd treat the homepage copy.
* **Write explicit negative framing where it prevents mischaracterization**: a short "what we don't do" list (e.g. we don't train models, don't build gambling platforms, don't do body-shopping) helps a model avoid confidently getting your positioning wrong.
* **Put a recurring review on the calendar.** Content drifts: services get renamed, case studies get added, methodology evolves. We keep a quarterly Linear ticket ("Quarterly review of llms.txt") specifically to re-read both files against the current site and correct anything stale.

Reference implementation: [`public/llms.txt`](https://marsbased.com/llms.txt) and [`public/llms-full.txt`](https://marsbased.com/llms-full.txt) on marsbased.com.

***

## Per-page Markdown (`.md` convention)

Alongside `llms.txt`, expose a clean Markdown version of every citation-worthy page by appending `.md` to its URL, e.g. `/services/ruby-on-rails` → `/services/ruby-on-rails.md`. Document this convention explicitly in `llms.txt` so a model (or a person) can derive it for any URL without guessing.

### Scope it to high-value pages

Not everything should get a Markdown twin. Exclude:

* **Ephemeral or low-value pages**: newsletters (their HTML is often messy, e.g. pasted-in Mailchimp markup), job listings that expire.
* **List-only pages**: blog tag/author index pages that are just links, not content.
* Point their `llms.txt` entries at the HTML page instead of a `.md` that isn't worth serving.

Keep the exclusion list as an explicit allow/deny function in code (path prefixes and exact matches), not scattered conditionals; it should be trivial to see, at a glance, what is and isn't exposed.

### Prefer build-time generation over render-on-demand

For anything prerendered (static pages), generate the `.md` file at build time as a sibling of the `.html` file, and let it get served straight off disk. Reserve on-demand rendering for genuinely dynamic (SSR) pages only. This one decision closes most of the "render amplification" attack surface described below: a bogus `.md` path for a static page is a cheap 404, not a render.

### Conversion quality

Server-render the page, extract the main content region, strip non-content nodes (`script`, `style`, `noscript`, `svg`, `iframe`, `form`, `button`, visually-hidden/decorative elements), and convert to Markdown (we use [Turndown](https://github.com/mixmark-io/turndown)). A few conversion details matter more than they look:

* **Emit one clean H1 from the page `<title>`** (brand suffix stripped, e.g. "Ruby on Rails - MarsBased" → "Ruby on Rails"), rather than keeping whatever in-page `<h1>` exists; this avoids duplicate or awkwardly-styled headings.
* **Prepend the meta description as a blockquote**, and always include a `Source:` line with the canonical URL, so a model that later quotes the page can attribute it correctly.
* **FAQ accordions** (`<details>`/`<summary>`) should convert to real headings (e.g. `## Question text`), not a flattened paragraph; this is what preserves the Q\&A structure that both models and `FAQPage` schema rely on.
* **Card titles wrapped in links** (`<a><h3>Title</h3></a>`) need a custom rule too, or Turndown's default nesting produces broken output like `[\n\n### Title\n\n](url)` instead of `### [Title](url)`.
* **Image-only content with no visible text** (e.g. a logo grid using `role="img"` + `aria-label` with a background-image) needs its `aria-label` promoted into a real text node before conversion, or the model gets an empty link.
* **Absolutize root-relative links** (`](/foo` → `](https://yoursite.com/foo`) so the Markdown is self-contained wherever it's fetched from.

### Security and performance hardening

An unauthenticated "render any path as Markdown" endpoint is a DoS and content-sniffing surface if you don't bound it:

* **`X-Content-Type-Options: nosniff`** on every `.md` response (and its 404): Markdown can contain passed-through HTML fragments; don't let a sniffing client reinterpret it as HTML.
* **A single, timeout-bounded self-fetch per request** (we use 5s) when rendering on demand: don't retry across multiple origins, since that just multiplies the cost an attacker can trigger per request.
* **A loop guard header** (e.g. `x-llms-md: 1`) on the internal self-fetch so a Markdown render can never trigger another Markdown render.
* **`redirect: 'manual'` on the self-fetch**, so a redirect alias (e.g. `/community` → `/company/community`) 404s instead of silently serving Markdown with a misleading `Source:`. The `.md` endpoint should only ever serve canonical URLs.
* **`X-Robots-Tag: noindex`** on `.md` responses: you want models fetching this content, not search engines indexing it as a duplicate page.
* **Cache aggressively at the edge**, including 404s, so repeated bogus `.md` requests are absorbed by the CDN rather than hitting your origin every time. Canonicalize the cache key (strip query strings) so `?x=1`, `?x=2`, … can't bust the cache.
* **Log self-fetch failures and 5xx upstreams as warnings**, distinct from a genuine 404; otherwise an infrastructure failure just looks like a missing page and is undebuggable.
* If the endpoint remains unauthenticated and render-on-demand for any path, note **edge rate-limiting on `*.md`** as a follow-up with infra: it's the cheapest additional mitigation once the above is in place.

### Test it like any other public endpoint

Cover at minimum: the URL-matching logic (what counts as a `.md` request), the exclusion/eligibility rules, title cleaning, the happy path (headers, content extraction, canonical source line), the loop guard, redirect-alias handling, and the error path (self-fetch throws, so it should produce a clean 404, not a crash).

***

## Robots.txt: don't block AI crawlers

Unless a client has a specific reason to opt out, allow AI crawlers the same way you allow search engines:

```plaintext
User-agent: *
Allow: /

Sitemap: https://www.example.com/sitemap-index.xml
```

A blanket `Allow: /` for `User-agent: *` already covers `GPTBot`, `ClaudeBot`, `PerplexityBot`, `Google-Extended`, and friends by omission; there's no strictly-necessary reason to enumerate them individually unless you want to grant or deny a specific one differently from the rest. Being cited by an AI answer is exposure, not theft; default to open.

That said, an *implicit* allow is a decision nobody actually made: it's worth making the choice explicit rather than relying on a blanket rule to cover crawlers it was never written with in mind. If a project cares about this, list the AI crawlers you've deliberately decided to allow (or, for a client with real reason to opt out, block) as their own `User-agent` blocks, rather than assuming the wildcard rule already reflects an intentional decision.

***

## A dedicated FAQ page, written to be quoted

Beyond FAQ accordions embedded in service pages, ship a standalone FAQ page answering the real questions your ICP (ideal customer profile) asks before hiring you: availability, team composition, how scoping/kickoff works, pricing approach, IP/NDA handling, how AI fits into your process, references, how you control scope creep, compliance status (e.g. GDPR, ISO 27001), and flexibility to scale up or down.

* **Each answer should stand alone as a quotable snippet**: a sentence or two that reads correctly even lifted out of context by an AI Overview, with no "as mentioned above" or other reference to surrounding content.
* **Answer the question directly first**, then add nuance; don't bury the actual answer under a paragraph of preamble.
* Mark the page up with `FAQPage` JSON-LD matching the visible `Question`/`acceptedAnswer` pairs exactly, in addition to whatever `WebPage` node the page already carries.

## Structured data and Knowledge Graph signals

GEO and schema.org markup reinforce each other: a model extracting your page benefits from the same clear entity/answer structure that search engines do. See our [schema.org implementation guidelines](/our-development-guides/schema) for the full approach, but a few patterns are specifically about being correctly resolved as an *entity* by AI systems, not just ranked as a page:

* **Enrich the sitewide `Organization` node** beyond the bare minimum: `legalName`, `description`, `foundingDate`, `foundingLocation`, `address`, `contactPoint`, `numberOfEmployees`, `knowsAbout` (your real areas of expertise), and a `founder` array of `Person` nodes (each with its own `@id` and `sameAs` linking to their real LinkedIn/GitHub). This is what strengthens Knowledge Graph eligibility and helps a model disambiguate who you are.
* **Never invent a field to fill out the schema.** If there's no on-site search, skip `SearchAction`. If you don't have a larger logo asset, don't fake the dimensions. Trust signals cut both ways: invented structured data is a liability the moment a model (or a person) cross-checks it against the visible page.
* **`FAQPage`** on any page with real Q\&A content: mark it up as `Question`/`acceptedAnswer` pairs matching the visible accordion content exactly.
* **Conversational, direct-answer content structure** in general: a clear question as a heading, followed immediately by a direct answer, is both good `FAQPage` markup and good Markdown once converted.

***

## Meta descriptions and titles written for lifting

AI Overviews and answer engines frequently lift your meta description verbatim as the summary shown alongside a citation. Treat these fields as GEO copy, not an SEO afterthought:

* **Titles**: aim for 50–60 characters, specific rather than generic (avoid boilerplate like "Home | Company Name" repeated across pages).
* **Descriptions**: aim for 150–160 characters, a direct, complete sentence describing the page, not a keyword list or a generic company tagline reused everywhere.
* Rewrite anything that reads like it was auto-generated or copy-pasted across page types; it reads just as poorly lifted into an AI answer as it does in a search result.

***

## QA checklist

Before shipping GEO work on a project:

1. `llms.txt` and `llms-full.txt` exist, are mirrored per locale if applicable, are curated (not templated boilerplate), and have a recurring review scheduled.
2. Every important page category (services, work/case studies, company, blog) has a working `.md` twin; low-value pages (newsletters, expiring listings, tag/author indexes) are explicitly excluded, not accidentally broken.
3. Static pages generate their `.md` at build time; only genuinely dynamic pages render on demand.
4. The `.md` endpoint has: `nosniff`, a bounded single self-fetch with a timeout, a loop guard, manual redirect handling, `noindex`, and edge caching (including on 404s).
5. Converted Markdown reads cleanly: one H1, correct brand-stripped title, a `Source:` line, no mangled links or empty image labels, no leftover script/style/decorative noise.
6. `robots.txt`'s stance on AI crawlers is a deliberate decision, not an accident of the wildcard rule.
7. A dedicated FAQ page exists with short, self-contained, directly-quotable answers to real ICP questions, marked up with `FAQPage` schema.
8. The sitewide `Organization` node is enriched with real, verifiable facts (founders, expertise, contact, address); nothing invented to fill a field.
9. Meta titles (50–60 chars) and descriptions (150–160 chars) are specific and non-generic per page, not boilerplate reused across page types.
10. Unit tests exist for the URL-matching, eligibility, and rendering logic; this is production code serving the public internet, not a one-off script.


# Our blogging guide

## The MarsBased Blogging Guide

We’ve been blogging for over ten years now, and we’ve learnt a thing or two. Because we’ve adopted transparency as an integral part of the company culture, we like sharing what we learn with you. It’s a genuine way to give back to the community that has helped so much become what we are now.

In this guide, we will cover how to create a blogging strategy for your company.

We’re really proud of how and what we blog, because it helps us keeping track of our evolution as a business and development consultancy, and feel like this could prove useful to many others that come after us. Exactly the same way we learnt from others like [Buffer](http://www.buffer.com), [Dockyard](http://dockyard.com), [Thoughtbot](http://thoughtbot.com) or other blogs we follow.

Previously, we published the [first part of our blogging guide](https://github.com/MarsBased/handbook/tree/main/blog/2015/05/05/The-MarsBased-Blogging-Guide/README.md). In the first part, we described how to craft a good blog post, by choosing a good title, the appropriate keywords, a fitting image, and other important criteria bloggers follow.

Blogging is nowadays a very important element in growing your company: it is one of the cheapest methods to create [inbound marketing](https://en.wikipedia.org/wiki/Inbound_marketing), provides SEO, and helps you prove your expertise to a very large audience.

In this guide, we’re going to describe how to choose a blogging strategy for your company.

## Choose a goal

First and foremost, one must identify what is the purpose of his/her blog. A blog can be a good tool to publish news about your company, how-to guides, inspirational stories, funny viral content or almost whatever you like.

One of the most common uses is content marketing. In this case, companies blog about the topics they want to be identified with and thus, they create new content for their site on a frequent basis. This content, in turn, attracts potential customers to their sales funnel. Google and most search engines like original content that updates regularly, hence being blogs a most useful tool.

At MarsBased we use it to drive qualified traffic to our website and attract talent, so we made it a key element in our sales toolset.

When we created the company, we didn’t have that much of a strategy, so we wrote posts about our company evolution: what we did and why we did it. However, some of these posts helped us rank high enough in search engines because they contained the right keywords. For instance, our most visited blog entry is [this one we wrote about our tech stack](https://marsbased.com/blog/2014/03/24/how-we-make-the-right-app2/).

We have received many project requests over the years because of our posts about technologies. In that post, we didn’t only show our expertise on the listed tools, but the content also made us rank higher on Google, especially in high-demanded technologies like Angular (back in 2014-15) or Sidekiq because they’re specific enough to be very visible to the few people that google them.

And that brings us to the next section.

**Key question:** what do you want your blog to be known for?

## Identify your target and blog for it

Once you’ve selected the purpose of your blog, you need to choose the audience. Who will read your blog?

Well, chances are you will want the whole world to read your blog from the get-go. However, it is a nearly impossible task to bring a generic blog to big audiences by making it grow organically.

If your target audience is the whole world, you are probably doing something wrong. You’d better hire somebody skilled in growth-hacking, or use paid marketing strategies (SEM) to achieve rapid growth. But we will not be covering this scenario in our guide.

A safer way is to choose a very defined audience and go for it. For instance, if your product is a fitness app, you should blog about fitness, nutrition and healthy lifestyles to attract your target audience: nutritionists, people that want to get fit, gym rats, personal trainers, and the like.

In our case, we want to build trust with companies we haven’t met yet and attract potential clients, by proving our expertise in web development. Therefore, we are sharing how we work, what we do, and things that are interesting to C-level personnel, our entry point in most companies.

Defining a target audience will also help you choose the kind of content and the language level. You can blog about marketing for the CMOs, tech stuff for CTOs and productivity for CEOs. Having multiple targets or multiple topics is not less effective, so long as it is part of your business strategy and you cater for all of them accordingly.

As for the language, technical posts might have technicisms, but it does not hurt to explain the acronyms or lead the reader somewhere where he/she can find the definitions (e.g: "If you don’t know what Inbound Marketing is, read this post over here…”). The more specific your target audience is, the more niche keywords you are allowed to use. Elsewise, if you’re targeting a massive audience, you will have to use very high-level and generic language, avoiding all sorts of acronyms, technical words or jargon.

**Key question**: What readers do you want to engage with through your blog and what do you need of them?

## Define your topics

Once you have chosen your goal and your audience, you must identify what will be interesting to your target readers.

The best blogs often cover just one specific topic. It might be travel, entrepreneurship, productivity, cuisine or knitting. But some blogs cover many topics to attract different sorts of target readers.

If you’re bootstrapping a company, you will most likely neither have the time to blog, nor the resources to have somebody do that for you. A good tip is to start small by covering a single topic. For instance, our buddies at [Quipu](https://getquipu.com/en), a personal finances app (built using Ruby on Rails!), chose to target small companies and freelancers/contractors that want to automate their bookkeeping. As a result of that, they are [blogging about personal finances](http://getquipu.com/blog/): how to do paperwork, how-to guides on taxes & forms, common mistakes in bookkeeping and tips about saving money on taxes are some of the topics they’ve dealt with.

It might look obvious, but if you write about startups and entrepreneurship, don't review your most favourite restaurants. It's OK to go offtopic every once in a while, but not in every second post.

The bottom line here is that choosing the right topics attracts like-minded people, it’ll rank great in Google and you can share it in niche groups or publications to be exposed to a larger audience.

**Key question:** What field(s) is your company most proficient in?

## Measure your tempo

It is a common mistake to start writing a blog with nothing to publish. Most people build up a few posts before launching the blog so that they can publish stacked posts for the first weeks, without having to worry about running out of content.

But even if you’re really committed to writing frequently, chances are, in your first blog you’ll write very effusively for some time and then will stop after a few weeks. That is why you should include the posting frequency in your strategy.

It is better to define a frequency and follow it strictly. For instance, at MarsBased we decided to post once a month a while back ago, and since earlier this year we’ve achieved it. Now we plan to blog twice a month to include a technical post, and a non-technical post to cater for our different targets.

A good piece of advice is to define a minimum, never a fixed number. It is better to say “I will post at least once every fortnight” than “I will publish 3 posts a month”. This way, if you're really prolific one month, you can go over your goal.

As you grow your company, you will also want to grow your content team. Likewise, depending on how important is the blog to your goals, you will increase the number of posts per month/week/day at a sustainable pace.

There are a few variations of the sentence “Content is king”. We’d like to add our bit to it: “Content is king, constancy is queen”.

**Key question:** How often can you afford to make your blog your first priority?

## Pick a date & time

Once you have defined your blog frequency, you need to select what days & times are better for your readers.

First of all, you should take into account your time zone. Say, if you’re an offshoring development company from India and your target is the UK, try posting in their morning, so that people read your blog first thing during breakfast or during their commute to work. Also, companies targeting US clients should choose between East & West Coast. A good tactic is to publish for East Coast first, and then publish the link in the social networks for each different time zone you might have defined as target (West Coast, Japan, etc.).

According to [Buffer](http://buffer.com) and other social media gurus, the best time to blog is either early in the morning or right after lunch. There does not seem to be a consensus on which days of the week are better for blogging. Test it for yourself.

Test, measure, repeat. [Google Analytics](http://www.google.com/analytics/), [Medium](http://medium.com) and many Wordpress plugins are really effective when measuring the impact of your blog posts. By using them, you can understand your audience and adjust your publishing times to them.

**Key question:** When are your target readers most likely to invest some time to read you?

## Produce content worth sharing

We don’t really know a lot about how to monetise a blog, so this tip goes to bloggers that want to get exposure organically.

One of our favourite social media gurus, Guy Kawasaki, explained what is the most effective tactic to be noticed on social networks. His advice is to [create content worth sharing](https://www.startupgrind.com/events/details/startup-grind-london-hosted-guy-kawasaki-canvaex-apple#/). Your content can be inspiring, educational, fun, viral, give free content or what have you, but it should really get your audience to spread the word for you.

Social media is your friend and can really boost your number of views and shares: aim for retweets or shareable content. Not only will the entry be good, but you can also test stuff outside of the content. For instance, [Buffer test their blog posts titles using the social networks](https://blog.bufferapp.com/a-scientific-guide-to-writing-great-headlines-on-twitter-facebook-and-your-blog). They share each blog entry with three or four different titles and perform A/B testing. The tweet that gets more visits, retweets, likes and favourites is the best title for the blog. This is only a way to A/B test it, but there are millions of ideas out there! Be creative!

Social networks also offer a great chance to engage with your community. For those old enough to have used internet forums back when they were the thing - in the pre-Google or pre-Myspace days - we saw people posting stuff in forums to get like-minded people’s opinions and interesting discussions. You can do the same here, by wrapping up the post with a call to action, as we said in the first part of our blogging guide.

We can also learn a lot from Instagram, where famous Instagrammers ask to “tag a friend” when that photo relates to them in any way. Hence, we can ask our friends or colleagues to comment on a post we wrote if we think it applies to them.

**Key question:** if you read this blog entry somewhere else, would you share it?

## Content must be educational and inspiring

One thing that works very well for me, when I am looking for new ideas, is to read a lot of blogs. From there, I always spot one or two blog entries that catch my attention and I want to mimic.

For instance, CartoDB’s entry of [“How we hire”](https://medium.com/@saleiva/how-we-hire-3e696c3aee59) inspired Pablo Villalba’s “How we hire” and, in turn, inspired me to include it in my list of Article Ideas on Apple Notes.

These posts were inspirational: not only do they make me want to write something similar for MarsBased, but they might also inspire us to adopt some new tools, strategies or techniques in our daily routines.

On the other side, the content needs to be also useful and teach you something new. It must educate. This is especially so for companies like ours that want to prove their technical skills before they get hired.

Good examples of educational blog posts are [this one](https://thoughtbot.com/blog/a-case-study-in-multiple-time-zones) and [this one](https://dockyard.com/blog/2015/07/31/taking-advantage-of-time-limitations-in-design), and we’re pretty sure they have helped those companies to earn a few more leads by demonstrating their expertise.

**Key question:** What are your favourite blogs and why?

## Make it visible

One tremendous mistake bloggers make is not letting the world know they have a blog.

Come on, you’ve spent time setting up a blog, creating a brand for it - if not company -, researching, writing content, picking nice images, tweaking its look & feel, selecting the keywords and all these chores, yet you don’t tell every soul about it.

Things you can do:

* Update all your social media profiles to link to your blog.
* Add it to your LinkedIn profile in the “URLs” section.
* Join blog listings.
* Post your content in LinkedIn/Reddit/Quora/Facebook groups.
* Automate your social network accounts to publish automatically every new entry using tools like IFTTT.
* Mention on social media other companies/individuals mentioned in the article, so they're notified about it (most people will engage with the content!)
* Add it to your signature.
* Etc.

**Key question:** Where do you want your blog to get noticed?

## Last but not least, what about AI?

Don't ever write stuff using AI. We use it only to generate new ideas of a specific topic or to discuss the pros and cons of writing a certain article via a conversation so you can refine your angle, but the writing is 100% done by humans.


# How to write a damn good blog post

At MarsBased, we are always learning new things. Running your own company means constantly staying in the loop and adapting to new challenges. Among other skills, we’ve learned how to run a successful blog.

Here’s a modern take on what we’ve learned.

***

### **Choose a Good Title**

Let’s start with the basics: the title. Titles need to be clear for both humans and search engines.

If you're writing a guide on how to craft good blog posts, go with something like:

* *How to Write Blog Posts That Drive Traffic*
* *10 Useful Tips for Crafting Blog Content*

Avoid obscure or overly complex titles like *Disentangling the Unfathomable Nature of Swordbuckling in the Interwebs*. Clear, meaningful titles attract more readers and improve SEO.

**Best Practices:**

* **Keep titles under 600 pixels** (about 55-70 characters) to avoid truncation in search engines.
* Include **target keywords** naturally to enhance visibility.
* Use capitalization that matches your audience's expectations (e.g., title case for most English audiences). We use British English capitalisation.

***

### **Catch Your Reader's Attention**

The first few sentences are critical. They set the tone and purpose of the post, while also affecting SEO. Search engines and social media often display the opening sentences as a preview.

**Tips for Success:**

* Use keywords naturally in the first paragraph.
* Hook readers with facts, anecdotes, or direct questions.
* Avoid clickbait—build trust by delivering on your promises.

***

### **Content**

Good content is shareable content. Blogs are essential for SEO because they provide fresh, original material that attracts engagement and backlinks.

**Modern Tips:**

* Focus on **content clusters**: group related posts under a central topic to boost topic authority.
* Write for **user intent**: prioritize helpful, relevant answers over keyword stuffing.
* Mention and link to companies or influencers in your content. Notify them via social media to potentially earn extra shares.

***

### **Links**

Internal and external links remain vital, but the focus has shifted:

**Internal Linking:**

* Use descriptive anchor text that gives context (e.g., *read more about SEO best practices* rather than *click here*).
* Link to related posts to improve site navigation and engagement.

**External Linking:**

* Link to authoritative sources to enhance credibility.
* Always open external links in a new tab using `target=_blank`.

Example of good HTML for links:

```html
<a href="https://example.com" title="Example Website" target="_blank">Visit Example Website</a>
```

***

### **Images**

Every blog post needs at least one image. Images enhance readability and engagement.

**Modern Image Practices:**

* Use modern formats like **WebP** for better compression and loading speeds.
* Include descriptive `alt` text for accessibility and SEO.
* Optimize image size with tools like **TinyPNG** or **ImageOptim**.
* Implement **lazy loading** to improve page speed.

Example HTML:

```html
<img src="path-to-image.webp" alt="Descriptive Alt Text" title="Descriptive Title">
```

***

### **Tags**

Tags help categorize your content but should be used sparingly and strategically.

**Best Practices:**

* Develop a flexible **tag taxonomy** that evolves with your blog’s focus.
* Tags should balance specificity and generality (e.g., *SEO* vs. *Technical SEO*).

***

### **Readability**

Blog posts should be easy to scan and read. Use subtitles, bullet points, and short paragraphs to improve clarity.

**Tools for Readability:**

* **Hemingway Editor** for readability improvements.
* **Yoast SEO** for SEO-friendly content structure.

***

### **Style**

There’s no universal "perfect length" for blog posts. Tailor the post length to its intent:

* **Short posts (300-500 words):** Quick updates or news.
* **Long posts (1500+ words):** Comprehensive guides or thought leadership.

**Tips:**

* Proofread thoroughly with tools like **Grammarly** or **LanguageTool**.
* Ask someone else to review for clarity and grammar before publishing.
* We use British English style for grammar, too.

***

### **Call to Action (CTA)**

Every post should have a clear purpose. Whether it’s educating readers, guiding them to a product, or encouraging them to share the post, include a CTA.

**Best Practices:**

* Use imperative verbs (e.g., "Learn more", "Download now").
* A/B test CTAs with tools like **PostHog**, **VWO**, or **Hotjar** to see what works best.

***

### **How to Contribute?**

Want to contribute to the MarsBased blog? Open a pull request with your draft and add Àlex as a reviewer.

We encourage weekly contributions, whether it's a guide, technical post, or company update—anything related to what we do, who we are, and what we believe in.


# AI Augmented Development

At MarsBased, all developers use [Claude Code](https://claude.ai/download) as our primary AI coding tool. Rather than treating AI as a glorified autocomplete, we follow a structured methodology that keeps humans in control of decisions while letting AI handle the heavy lifting.

This guide covers how we work with AI coding agents, the methodology we follow, and how we set up our environment for consistent results.

## The Research / Plan / Implement (RPI) methodology

Unstructured "vibe coding" with AI agents produces unreliable results. We follow the **Research / Plan / Implement** framework to keep quality high and mistakes contained.

Each phase runs in a fresh context window so that irrelevant information from previous steps does not degrade output quality.

### Research

Map the problem space without writing any code. The goal is to understand the codebase, dependencies, data flows, and constraints relevant to the task. Document findings so they can be carried into the next phase.

* Explore files, dependencies, and architecture related to the task.
* Summarize what you found: what exists, what's missing, what's tricky.
* Do not write implementation code during this phase.

### Plan

Create a detailed, step-by-step execution plan based on your research. We use Claude Code's **plan mode** (`/plan`) for this phase. Plan mode forces Claude to think through the approach and produce a structured plan before writing any code.

The plan should be specific enough that implementation becomes almost mechanical. A bad step in a plan produces hundreds of wrong lines of code, so catching mistakes here is far cheaper than catching them later.

* Break the work into numbered, sequential steps.
* Each step should reference specific files and describe the change.
* Instruct Claude to ask you whenever it faces a decision with multiple possible choices, rather than picking one silently. This keeps you in the loop on trade-offs and prevents wrong turns.
* Review the plan before moving on. Does it solve the right problem? Is the approach sound?

### Implement

Execute the plan. For larger tasks, break implementation into chunks, each in a separate context window, to keep context utilization manageable.

* Follow the plan step by step.
* Flag deviations: if you need to diverge from the plan, note why.
* Review the output against both the requirements and the plan.

### Three review points

Human judgment applies at three moments, not just at the end:

1. **After Research** — Are we solving the right problem?
2. **After Plan** — Is the approach sound?
3. **After Implementation** — Does the output match requirements and plan?

## CLAUDE.md and rules

Claude Code starts every session with a fresh context window. `CLAUDE.md` files give it persistent instructions so you don't re-explain the same things every time.

We maintain a project-level `CLAUDE.md` in every repository with:

* Build and test commands.
* Project conventions and architecture decisions.
* Common workflows and gotchas.

For more granular control, we use `.claude/rules/` files scoped to specific file types or directories (e.g., testing conventions that only load when working on test files).

Keep instructions concise, specific, and verifiable. "Use 2-space indentation" works better than "format code nicely."

## Context management

The context window is finite. How you manage it directly affects output quality. Key practices:

* **Start fresh for new tasks** — Use `/clear` when switching to an unrelated task.
* **Rewind over correction** — If Claude goes down a wrong path, use `/rewind` to go back instead of asking it to fix its own mistakes. Then re-prompt with what you learned.
* **Compact when needed** — Use `/compact` when a session feels bloated with stale debugging context.
* **Delegate noisy work to subagents** — When a task generates lots of intermediate output you won't need again (large searches, verification), let a subagent handle it so your main context stays clean.

## Skills and subagents

**Skills** are reusable, project-specific workflows packaged as prompts. They load on demand when you invoke them (e.g., `/review`, `/commit`) rather than occupying context at all times. We use skills for repetitive workflows like code reviews, commit message generation, and PR creation.

**Subagents** are independent Claude Code instances that run in isolation with their own context window. A useful mental model: treat a subagent the way you would treat someone you're delegating work to. When you identify a task you would hand off to a colleague — self-contained, well-defined, with a clear deliverable — that's a good candidate for a subagent. You scope the work, hand it over, and get back a result without being involved in the intermediate steps.

A concrete example is the `pr-code-reviewer` agent. When a PR is ready for review, open a terminal, start Claude, and share the GitHub PR URL. The agent fetches the diff, evaluates it against a defined checklist — security, naming, test coverage, SOLID principles, commit conventions — and returns a structured review draft. Code review becomes something you delegate rather than something you do yourself.

We don't default to subagents. We only reach for them when there is a clear reason:

1. **Parallelization** — When multiple independent tasks can run simultaneously (e.g., researching two unrelated parts of the codebase at once).
2. **Specific isolated tasks** — When a well-defined task would generate large amounts of intermediate output that would pollute the main session (e.g., broad codebase searches, running verification suites).

Only the final result of a subagent returns to your main context, keeping it clean. If the task doesn't clearly benefit from parallelization or isolation, work directly in your main session.

## Sandboxing

By default, any shell command Claude Code runs has the same access you do: your whole filesystem, your whole network. We enable sandbox mode on all machines to contain that.

The sandbox is an OS-level isolation layer for Bash commands and their child processes — Seatbelt on macOS, bubblewrap on Linux and WSL2. It is enforced by the operating system, not by the model, so it holds regardless of what Claude decides to run or whether a command does more than its name suggests.

It has two layers:

* **Network** — Traffic from sandboxed commands goes through a proxy that enforces a domain allowlist. No domains are pre-approved: the first time a command needs to reach a new domain, Claude Code asks for approval, and approving allows that host for the rest of the session.
* **Filesystem** — Commands can only write to the working directory and the session temp directory. No writing to `~/.zshrc`, `/bin`, or anything outside the project.

### Enabling it

Enable it globally in your user settings at `~/.claude/settings.json`:

```json
{
  "sandbox": {
    "enabled": true,
    "filesystem": {
      "denyRead": [
        "~/.aws",
        "~/.ssh",
        "~/.gnupg",
        "~/.netrc",
        "~/.git-credentials",
        "~/.config/gh",
        "~/.config/gcloud",
        "~/.docker/config.json",
        "~/.kube/config",
        "~/.npmrc",
        "~/.pypirc",
        "~/.cargo/credentials.toml",
        "~/.azure",
        "~/.terraformrc",
        "~/.terraform.d"
      ]
    },
    "credentials": {
      "envVars": [
        { "name": "GITHUB_TOKEN", "mode": "deny" },
        { "name": "GH_TOKEN", "mode": "deny" },
        { "name": "AWS_ACCESS_KEY_ID", "mode": "deny" },
        { "name": "AWS_SECRET_ACCESS_KEY", "mode": "deny" },
        { "name": "AWS_SESSION_TOKEN", "mode": "deny" },
        { "name": "NPM_TOKEN", "mode": "deny" },
        { "name": "ANTHROPIC_API_KEY", "mode": "deny" },
        { "name": "OPENAI_API_KEY", "mode": "deny" },
        { "name": "RENDER_API_KEY", "mode": "deny" },
        { "name": "DATABASE_URL", "mode": "deny" },
        { "name": "RAILS_MASTER_KEY", "mode": "deny" },
        { "name": "SECRET_KEY", "mode": "deny" }
      ]
    }
  }
}
```

The two blocks cover the two ways a secret leaks into a shell command. `denyRead` blocks sandboxed commands from reading credential files and directories on disk. `credentials.envVars` unsets those variables before each sandboxed command runs, so a token exported in your shell is not inherited by whatever Claude executes. Deny entries merge across every settings scope and only ever narrow access, so a project can add to this list but never remove from it.

A denied variable is gone entirely for sandboxed commands, which will break a tool that genuinely needs it. If that happens, drop that single entry rather than removing the block.

You can also run `/sandbox` inside a session, which opens a panel to configure it per project and tells you whether any dependency is missing. Choose Auto-allow mode: sandboxed commands run without prompting because the sandbox boundary is what contains them, and anything that can't be sandboxed falls back to the regular permission flow.

On Linux and WSL2, install the dependencies first with `sudo apt-get install bubblewrap socat` and restart Claude Code. Native Windows is not supported — run Claude Code inside WSL2.

Pre-approve the domains you always need with `sandbox.network.allowedDomains` to avoid repeated prompts, and grant extra write paths with `sandbox.filesystem.allowWrite` when a tool genuinely needs them. Keep both lists narrow: a broad allowed domain is a data exfiltration path, since the proxy does not inspect TLS traffic by default.

### What it does not cover

The sandbox applies to Bash commands only. Claude Code's built-in tools — Read, Edit, Write, WebFetch — along with MCP servers and hooks run in-process and are governed by the permission system instead.

Within the sandbox, only writes are restricted by default: commands can read the entire machine unless you say otherwise, and there is no built-in credential deny list. That is why the `denyRead` and `credentials` entries above are part of the baseline config rather than an optional extra.

## Hooks

Hooks are shell scripts that Claude Code runs automatically at specific points during a session — before reading a file, after writing one, when a tool is called, and so on. They run outside of Claude's context and cannot be overridden by prompts, which makes them the right place to enforce hard rules.

We use hooks for two main purposes:

* **Security** — to prevent Claude from reading files that contain secrets or credentials, regardless of what the task is or what it is asked to do.
* **Static analysis and formatting** — to run tools like ESLint (with [ESLint Stylistic](https://eslint.style/) for formatting) or RuboCop automatically after Claude modifies a file, so the codebase stays consistent without Claude having to remember to do it. For example, we run `eslint --fix` on every TypeScript file Claude edits.

### Pre-read hook

Our `pre-read` hook runs before Claude reads any file. It blocks access to:

* **Credential files** — any file whose name starts with `.env` (`.env`, `.env.local`, `.env.production`, etc.) and `.netrc`.
* **Sensitive directories** — `.ssh`, `.aws`, `.gnupg`, `.kube`, and `.docker`.

If Claude tries to read a blocked path, the hook exits with a non-zero code and returns a `BLOCKED:` message. Claude Code surfaces this as an error and does not proceed with the read.

The hook lives at `.claude/hooks/pre-read.sh` in the project repository and is wired up in `.claude/settings.json` under the `hooks` key.

## Resources

* [How I work with AI coding agents (Daz)](https://daz.is/blog/how-i-work-with-ai-coding-agents/) — Deep dive into the RPI framework: how to structure research, write effective plans, and implement in chunks while keeping context quality high.
* [Research, Plan, Implement (Tyler Burleigh)](https://tylerburleigh.com/blog/2026/02/22/) — Practical walkthrough of RPI with concrete examples of `RESEARCH.md` and `PLAN.md` artifacts, phased implementation, and git-based rollback strategies.
* [Session management and the 1M context window](https://claude.com/blog/using-claude-code-session-management-and-1m-context) — Anthropic's guide to managing Claude Code sessions: when to clear, rewind, compact, or delegate to subagents.
* [How Claude remembers your project](https://code.claude.com/docs/en/memory) — Official documentation on CLAUDE.md files, `.claude/rules/`, auto memory, and how to write effective persistent instructions.
* [Configure the sandboxed Bash tool](https://code.claude.com/docs/en/sandboxing) — Official documentation on the Bash sandbox: filesystem and network isolation, settings reference, and managed deployment options.
* [Claude Code in Action](https://anthropic.skilljar.com/claude-code-in-action) — Anthropic's hands-on training course covering Claude Code architecture, tool usage, context management, MCP servers, and GitHub integration through video lessons and projects.


# Coding guidelines

Writing code can be as personal as hand-writing but we like to keep everyone within the same baseline. From our experience, some good ideas, patterns and styles we built a cool baseline that we believe it makes us better engineers, not only better programmers.

Focus on code readability. Code should tell a story. Code should be easy to understand and reason about by anyone.

* **1.** [Do's and Don'ts](#1-dos-and-donts)
  * **1.1** [Do's](#11-dos)
    * **1.1.1** [Keep code simple](#111-keep-code-simple)
    * **1.1.2** [Naming is key](#112-naming-is-key)
    * **1.1.3** [Remove unused code](#113-remove-unused-code)
    * **1.1.4** [Write meaningful code comments](#114-write-meaningful-code-comments)
    * **1.1.5** [Keep it small](#115-keep-it-small)
  * **1.2** [Don'ts](#12-donts)
    * **1.2.1** [Refactor ahead of time](#121-dont-refactor-ahead-of-time)
    * **1.2.2** [Premature optimization](#122-premature-optimization)
    * **1.2.3** [Unnecessary dependencies](#123-unnecessary-dependencies)
    * **1.2.4** [Don't comment code to remove it, just delete it](#124-dont-comment-code-to-remove-it-just-delete-it)
    * **1.2.5** [Refactor and add features](#125-refactor-and-add-features)
    * **1.2.6** [Don't make typos](#126-dont-make-typos)
    * **1.2.7** [Avoid comments with TODOs](#127-avoid-comments-with-todos)

## 1. Do's and Don'ts

### 1.1 Do's

#### 1.1.1 Keep Code Simple

Code should be simple! Easy to understand. Variables and methods with good naming. Methods short, classes small. Code should be properly organized for this purpose. Sometimes we can think of some design patterns that could be applied or some (complex) abstractions but these can lead to over-engineering. Your code as a purpose, focus on that. Let the patterns, the abstractions, the complexity come to you instead of driving you.

[Keep it simple, stupid](https://en.wikipedia.org/wiki/KISS_principle).

#### 1.1.2 Naming is key

Spend some time looking for good classes, methods and variables names. Make use of long and descriptive names for both functions/methods and variables. Never use 1 char variables! Remember you are writing code for others and for you, make it the best. Push to give the readers a good time.

Naming is key!

#### 1.1.3 Remove unused code

When we stop using a feature or part of it we should remove that code and any other unused code as a consequence. Remember, we don't want "dead" code around. It will give you a hard time later when you need to comprehend it again.

#### 1.1.4 Write meaningful code comments

Don't comment the WHAT about a piece of code, only the WHY, and if necessary. Comments should exist if you believe that the solution you've chosen might need extra context to be understood. Always focus on the WHY instead of the WHAT as this last one is already there, is our code.

#### 1.1.5 Keep it small

Less code the better, less code changes the better. As we like to build one thing at a time we should also built it as simple and focused as possible. Less code makes it better to understand, less code changes make it much more accessible for you and our colleagues as reviewers.

### 1.2 Don'ts

#### 1.2.1 Don't refactor ahead of time

Wait until something is painful to change or understand. Remember that we should keep it small and simple. Refactor without a purpose can lead to complexity, unnecessary abstractions and miscommunication about what you are trying to achieve.

#### 1.2.2 Premature optimization

In general don't do premature optimization, but always keep in mind that there are some basic techniques that can have big impact. Avoid N+1 queries (take advantage of the logs during development for this). Filter using SQL as long as possible (but keep a balance to avoid over-complicating the code). There are problems that can be avoided at first but don't go to deep blindly if there isn't any reason to.

#### 1.2.3 Unnecessary dependencies

Before adding a 3rd party library or a dependency think carefully if it's really necessary and the compromises you are making. We don't want to re-invent the wheel but sometimes it could be better to build what we need instead of depend on some library that do that and much more.

When adding a new library to an application, make sure:

* It's well maintained (check when the last commit was made and the number of open issues).
* It's not unnecessarily big and does not introduce a lot of extra dependencies under the hood.
* The license fits within the policy used in the application. Some projects only allow to have libraries using the MIT license, for example. Before adding a library with a more restrictive license, check with the team.

#### 1.2.4 Don't comment code to remove it, just delete it

We have git for god's sake. We can always check a previous commit to find the deleted code.

#### 1.2.5 Refactor and add features

Don't build new features and refactor at the same time unless that feature demands it. Focus on your current code and use code refactoring as a tool to achieve our goal. Refactor an unrelated part of the code that has nothing to do with that feature will not bring any value to you, to the reviewer and your codebase.

#### 1.2.6 Don't Make Typos

Be careful with your typing. Writing classes/methods/variable names, code comments, literal string/page copies and documentation should be free of typos.

Let your editor help you by installing a spell checker. Check [this plugin](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker) if you are using Visual Studio Code. Remember, checking typos is part of code reviews.

#### 1.2.7 Avoid comments with TODOs

If there is something that you think it should be done create an issue for it. Maybe it will be done right after, or maybe some weeks after. Let that decision be where it belongs, in your project management tool. Your code is not the place to leave tasks descriptions or memos.


# Security guidelines

## Personal Security

Good security practices start on yourself and your own devices (laptop, mobile phone, etc.). Any device you use to do MarsBased work needs to be properly protected to minimize the chances of a bad actor accessing the device data.

* [Protect Yourself](https://github.com/MarsBased/handbook/tree/main/guides/security/protect_yourself.md)
* [Protect Your Devices](https://github.com/MarsBased/handbook/tree/main/guides/security/protect_your_devices.md)

## Web Application Security

It is crucial to keep the web applications we work on as secure as possible. Even the smallest vulnerability, if exploited in the right way, can be disastrous for a client. Therefore, applying good security practices to the web applications we develop for clients (and even internal applications for MarsBased) is of the utmost importance.

* [Web Application Security Features](https://github.com/MarsBased/handbook/tree/main/guides/security/web_application_security_features.md)
* [Common attack vectors](https://github.com/MarsBased/handbook/tree/main/guides/security/common_attack_vectors.md)
* [Admin panel protection](https://github.com/MarsBased/handbook/tree/main/guides/security/admin_panel_protection.md)
* [User accounts protection](https://github.com/MarsBased/handbook/tree/main/guides/security/user_accounts_protection.md)
* [Cookies best practices](https://github.com/MarsBased/handbook/tree/main/guides/security/cookies_best_practices.md)
* [Security related headers](https://github.com/MarsBased/handbook/tree/main/guides/security/security_related_headers.md)
* [Periodical Maintenance Tasks](https://github.com/MarsBased/handbook/tree/main/guides/security/periodical_maintenance_tasks.md)
* [Securing backups](https://github.com/MarsBased/handbook/tree/main/guides/security/securing_backups.md)
* [Sensitive data in logs](https://github.com/MarsBased/handbook/tree/main/guides/security/sensitive-data-logging.md)

## MarsBased security

There are some cross-client aspects we need to take into account at company level to make sure we minimize the exposure of MarsBased or clients projects and data.

* [3rd Party Software Integrations](https://github.com/MarsBased/handbook/tree/main/guides/security/3rd_party_software_integrations.md)
* [Off-Boarding](https://github.com/MarsBased/handbook/tree/main/guides/security/off_boarding.md)


# Our Git & Commit guidelines

This is a guide covering how we expect to work with Git at MarsBased. Most of this guide is GitHub oriented but might be adapted to other Git tools like GitLab or Bitbucket.

* **1.** [Commit Message Guidelines](#commit-message-guidelines)
* **1.1** [Message Format](#commit-message-format)
* **1.1.1** [Message Header Type](#type)
* **1.1.2** [Message Header Scope](#scope)
* **1.1.3** [Message Samples](#commit-message-samples)
* **2.** [Git Branches Naming](#git-branches-naming)
* **3.** [Git Workflow](#git-workflow)
* **4.** [Dangerous behaviours](#dangerous-behaviours)
* **5.** [Credits](#credits)

At MarsBased we work with a large variety of clients. Some clients may follow their own guidelines. You can always suggest improvements over their guidelines but there will be cases where it will not be possible to use ours.

## Commit Message Guidelines

We have very precise rules on how git commit messages should be formatted. This leads to more readable messages that are easy to follow when reviewing the project history.

We use **Linear** as our tracking tool. Whenever possible, commit messages should reference the related Linear issue.

### Commit Message Format

Each commit message consists of a header and an optional description.

For squashed commits after merging a Pull Request, the commit message **must** follow this format:

```
[issue-code] <type>(<scope>): <subject>

<description>
```

Where:

* `issue-code` is the Linear issue identifier, for example `MARS-456`
* `type` is mandatory
* `scope` is optional
* `subject` is mandatory

If the commit is not related to a Linear issue, the `issue-code` could be ignored but only if the commit is not a feature commit (`feat`).

The maximum length of the header must be 72 characters. Any other line of the commit message must not exceed 100 characters. This improves readability in GitHub and other git tools.

The language used in commit messages is English. If the client needs access to the commit history for documentation purposes and does not understand English, other languages may be used instead.

### Type

Choose the type that best fits the task:

* **fix**: Represents a bug fix.
* **feat**: Adds a new feature.
* **deploy**: Changes related to the deployment process.
* **chore**: Dependency upgrades, refactors or maintenance tasks.
* **docs**: Documentation-only changes.
* **test**: Adding or fixing tests.

### Scope

The scope describes the specific module or part of the application affected by the change. It is optional.

Examples:

* **admin**: Admin panel.
* **users**: User management.
* **payment**: Payment gateway.

### Commit Message Samples

With a Linear issue:

```
[MARS-456] fix: review the commits documentation in handbook
```

```
[MARS-789] feat(admin): add users CRUD

We can now manage users through the admin panel.
We have added a new search module with an integrated calendar to be able to
filter entities by creation date.
```

Without a Linear issue (`feat` is not allowed):

```
fix: update Docker base image to v2.6
```

## Git Branches Naming

Any branch created for a project must follow these rules.

### Branches related to a Linear issue

Use the branch name provided by Linear or the client project tracking tool:

Examples:

* `mars-456`

This ensures that during the code review process, reviewers can simply copy the branch name from Linear and check out the code locally.

### Branches not related to a Linear issue

Use one of the following formats:

* `feat-short-description`
* `fix-short-description`

Examples:

* `feat-add-users-crud`
* `fix-logout-for-oauth-users`

For client projects, adapt the naming to their requirements if needed.

## Git Workflow

We use a simplified version of Gitflow.

For large projects already deployed to production, there are usually two long-lived branches:

* `main` (or `master` in older projects), which contains deployed code
* `development`, which contains the latest stable changes

For small or not-yet-deployed projects, it is acceptable to work directly on `main`.

Recommended workflow:

1. Create a new branch from the appropriate base branch.
2. Make as many commits as needed to complete the task. Commit message naming is not enforced at this stage.
3. Optionally open a draft Pull Request to get early feedback.
4. Rebase and clean up commits before requesting review. Leaving a single commit is acceptable.
5. Rebase your branch on top of the latest target branch.
6. Open a Pull Request for review.
7. Squash and merge. The resulting squashed commit message must follow the Commit Message Format.

If the project uses a `development` branch, merging to `main` should be done as follows:

1. Open a Pull Request from `development` to `main`.
2. Once all checks and reviews pass, merge using a merge commit strategy.
3. Create a new GitHub release and tag pointing to `main`.

For small projects or small teams, we usually keep things simpler by working with a single long-lived branch, typically `main`, and skip a separate development branch. This reduces overhead and makes day-to-day work and releases easier to manage.

Even in this simplified setup, we still work with short-lived branches per feature or fix, open Pull Requests, and perform code reviews before merging into `main`.

## Dangerous behaviours

* Avoid using `git push -f` when working on a shared branch. Use `git push --force-with-lease` instead.

## Credits

This guide is heavily influenced by:

* [Angular Commit Message guidelines](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines)
* [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/)

Some parts are adapted from those sources. All credit goes to the original authors 🙌


# Code reviews guidelines

This guide describes some best practices we apply when doing code reviews.

## How to perform a code review

* Review all the code, line by line, and apply the [checklist](#checklist) to all the reviewed code.
* Test all the code: Check both that there are no exceptions and that the correct result is produced.
* Check it correctly solves the issue it addresses. No more and no less.
* Check the produced architecture:
  * It is simple enough, not over-engineered.
  * It is easy to understand.
  * It is easy to extend.
  * It is not trying to predict the future. It just fits the issue it is addressing.
* Check if it has enough automatic tests.

It's useful to take an *algorithmic* approach when reviewing the code: take one line, apply the checklist, move the next, and repeat.

## Testing the code

Testing the code while doing a review is not something that all companies do, mainly because it can be time consuming. At MarsBased we strongly think it is important to test the code when doing a review. However, take into account that depending on how critical / complex the code is and how difficult it is to test, testing should be less or more exhaustive.

It is a matter of analyzing the cost / benefit ratio and applying common sense to it (is it worth spending a lot of time testing it? it depends on how critical the code is).

## Checklist

This is the main things you should be looking out for in the reviewed code:

* Code additions: Is the new code used?
* Code deletions:
  * Have all uses of the code been removed?
  * Can more now-unused code be removed?
* Typos: Are there any typos in methods / classes / variables names, comments, literals and documentation?
* Good practices: Is the code DRY, with short methods and small classes?
* Code style: Does the code follow the MarsBased conventions and it's idiomatic according to the programming language?
* I18n: Are all strings I18n-ized? (when doing a review for back-end code).
* Background jobs: Are background jobs idempotent? (when doing a review for back-end code).
* Naming: Do classes, methods and variable names use proper naming?
* Database migrations: Do database migrations apply correct constraints that follow model validations? (when doing a review for back-end code).
  * Are changes to schema.rb correct and only related to this PR? (when doing a review for Rails code).

You might have other things that you usually check for. The key idea is that it's useful to have a written list with all those points and apply it consistently without having to think about it every time.

## Comments in code reviews

When writing comments:

* Use "we", not "you". The software engineer is not alone, you are a team.
* Provide objective reasons that support the change. "Because I don't like it" is not a reason.
* Provide an alternative or a sketch of how you would do it instead.
* Don't write only negative comments, write also positive comments:
  * Positive comments increase motivation.
  * Positive comments encourage the software engineer to keep doing the right thing.

Example of a not constructive comment:

> you made this too complex, simplify

Example of a constructive comment:

> This can be simplified, we don't need to extract the numeric values for the status:
>
> * For assignment status we can directly do `where(status: %[preselected selected])`.
> * For appointment status we need to use `merge` otherwise will not work. Instead of `.where(customer_jobs_appointments: { status: appointment_statuses_keys })` we can do `.merge(CustomerJobs::Appointment.where(status: %[warning ready]))`.
>
> Also with these changes we don't need the rubocop disable flag.


# Testing guidelines

## Philosophy

Tests are documentation. They should clearly communicate what the code does and serve as living documentation for future developers. Prioritize readability and maintainability over cleverness.

## Test structure

Write tests as 3 separate blocks: **Arrange, Act, Assert** (also known as Given-When-Then).

```ruby
# Ruby (RSpec)
it "returns the user full name" do
  # Arrange
  user = create(:user, first_name: "John", last_name: "Doe")

  # Act
  result = user.full_name

  # Assert
  expect(result).to eq("John Doe")
end
```

```python
# Python (pytest)
def test_returns_user_full_name():
    # Arrange
    user = User(first_name="John", last_name="Doe")

    # Act
    result = user.full_name()

    # Assert
    assert result == "John Doe"
```

```typescript
// TypeScript (Jest/Vitest)
it("returns the user full name", () => {
  // Arrange
  const user = new User({ firstName: "John", lastName: "Doe" });

  // Act
  const result = user.fullName();

  // Assert
  expect(result).toBe("John Doe");
});
```

## Do's

* **Write boring tests.** The more boring a test is, the better.
  * Avoid abstractions unless there is a very good reason.
  * Repetition is acceptable. Copy & paste is allowed in tests.
  * Only extract to helper methods when it genuinely improves clarity.
  * Extracting large payloads or complex setup is fine.
* **Use human names** like "John" instead of "Candidate 1" or "User A". It makes tests more readable and creates a narrative.
* **Test against hardcoded values**, not dynamic references:

  ```ruby
  # Ruby - Good
  expect(json["name"]).to eq("John")

  # Ruby - Bad
  expect(json["name"]).to eq(user.name)
  ```

  ```python
  # Python - Good
  assert response["name"] == "John"

  # Python - Bad
  assert response["name"] == user.name
  ```

  ```typescript
  // TypeScript - Good
  expect(json.name).toBe("John");

  // TypeScript - Bad
  expect(json.name).toBe(user.name);
  ```
* **Mock every external call** to the internet (HTTP, gRPC, external APIs). Use libraries like:
  * Ruby: `webmock`, `vcr`
  * Python: `responses`, `httpretty`, `pytest-httpserver`
  * TypeScript: `msw`, `nock`
* **Test behavior, not implementation.** Focus on what the code does, not how it does it internally.
* **Keep tests fast.** Slow tests discourage running them frequently. Use unit tests for most coverage and reserve integration tests for critical paths.
* **One assertion per concept.** It's fine to have multiple `expect`/`assert` statements if they verify the same logical concept.

## Don'ts

* **Don't write "should" in test descriptions:**

  ```ruby
  # Ruby - Wrong
  it "should return the calculation result"

  # Ruby - Right
  it "returns the calculation result"
  ```

  ```python
  # Python - Wrong
  def test_should_return_calculation_result():

  # Python - Right
  def test_returns_calculation_result():
  ```

  ```typescript
  // TypeScript - Wrong
  it("should return the calculation result", ...)

  // TypeScript - Right
  it("returns the calculation result", ...)
  ```
* **Don't use faker/random value generators** (Faker, Chance, etc.) for most tests.
  * They can introduce flaky tests.
  * Hardcoded values help create a consistent story in the test suite.
  * **Exception:** Property-based testing / fuzz testing, where you intentionally run tests with random values to discover edge cases. Libraries: `hypothesis` (Python), `fast-check` (TypeScript), `rantly` (Ruby).
* **Don't test more than one thing per unit test.** Integration tests may verify multiple things since they are expensive to run.
* **Don't test private methods directly.** Test them through the public interface.
* **Don't share state between tests.** Each test should be independent and able to run in isolation.
* **Don't ignore flaky tests.** Fix them immediately or delete them. A flaky test is worse than no test.

## Test naming conventions

Use descriptive names that explain the scenario and expected outcome:

```ruby
# Ruby (RSpec)
describe User do
  describe "#full_name" do
    context "when user has both names" do
      it "returns first and last name concatenated" do
      end
    end

    context "when last name is missing" do
      it "returns only the first name" do
      end
    end
  end
end
```

```python
# Python (pytest)
class TestUser:
    class TestFullName:
        def test_returns_concatenated_names_when_both_present(self):
            pass

        def test_returns_first_name_when_last_name_missing(self):
            pass
```

```typescript
// TypeScript (Jest/Vitest)
describe("User", () => {
  describe("fullName", () => {
    it("returns first and last name concatenated when both present", () => {});

    it("returns only the first name when last name is missing", () => {});
  });
});
```

## Test organization

* **Unit tests:** Test individual functions/methods in isolation. Should be the majority of your tests.
* **Integration tests:** Test how components work together (e.g., API endpoints, database interactions).
* **End-to-end tests:** Test complete user flows. Use sparingly as they are slow and brittle.

Follow the testing pyramid: many unit tests, fewer integration tests, minimal E2E tests.

**When testing capacity is limited:** If a project's constraints (time, budget, team size) don't allow for comprehensive test coverage, prioritize E2E tests over unit tests. While slower and more brittle, E2E tests cover the most critical user paths with the fewest tests, providing maximum value per test written.

## What to test

* **Do test:** Business logic, edge cases, error handling, public APIs.
* **Don't test:** Framework code, simple getters/setters, third-party libraries.

## Database in tests

* Use transactions to rollback after each test when possible.
* Create only the data you need for each test.
* Use factories/fixtures to simplify data creation:
  * Ruby: `factory_bot`
  * Python: `factory_boy`, `pytest-factoryboy`
  * TypeScript: `fishery`, custom factories

## Continuous Integration

* All tests must pass before merging.
* Run the full test suite on every pull request.
* Keep the CI pipeline fast (< 10 minutes ideally but it depends on the project size).


# Our Docker guides

At MarsBased we use Docker to work on the development of applications. Using Docker for development has several benefits:

* Makes setting up the environment for a project a breeze. This dramatically reduces the time to onboard new software engineers to a project.
* The environment in which the application runs can be identical to the production environment. This allows to quickly detect bugs depending on the OS or system packages.
* Removes the need to have development dependencies (Ruby, Node, PostgreSQL, Redis) installed locally. Only Docker is needed to work on applications.
* Avoids problems with having multiple versions of dependencies installed. Moreover, prevents them from accumulating when versions are updated.

The approach that we follow to configure the Docker environment is heavily inspired by this [magnificent blog post from our evil colleagues](https://evilmartians.com/chronicles/ruby-on-whales-docker-for-ruby-rails-development).

The key point of this way working is that **the Docker image is kept as minimal as possible and dependencies are installed on volumes**.

By working this way, the image hardly ever needs to be re-built and we work very closely to how we would do it if we were working with local dependencies. For example: Instead of installing Ruby gems in the image, we mount a volume on the container to store the installed gems and run `bundle install` on the container.

If gems were installed in the image then when there is a change in the used gems (like adding a new gem or updating one), the image needs to be rebuilt, thus needing to install **all gems** again (which can take a long time). By, instead, having gems installed on a volume, when there is a change in a gem, we can just run `bundle install` again (like we would do locally) and it will just install that new gem or updated version.

It is useful to think of it as if the Docker image is just the bare-bones OS and you do the rest the same way you would do it locally (`bundle install`, `npm install`, etc.). More specifically, the image only contains the programming language and system packages (like ImageMagick).

There are 2 options to set up the development environment with Docker:

* **Services only:** External services (database, Redis, Minio, Elastic Search, etc.) are run with Docker but the application runs locally.
* **Services + Application:** Apart from services, the application is also run in a container.

Running everything with Docker has the advantage of not needing to have any dependency installed locally, apart from Docker. The disadvantage is that it runs slower because the application can't use the full memory + CPU potential from the computer.

When working on a single application it makes sense to use the services only approach, while when working on multiple projects it is more convenient to run all applications in containers to avoid having a dependency hell in the computer.

## Services only development setup

In order to Dockerize the services for development application, we create a `.dockerdev` directory in the application root, which contains the `docker-compose.yml` and other support files. Separating it into its own directory avoids mixing it with the production Docker setup which usually resides in the root.

When copying the files from this repo you need to replace several values for the appropriate in your application. These values are: `<application-name>`, `<postgres-version>`, `<redis-version>`.

The `.dockerdev` directory contains the following files:

* `docker-compose.yml`: Contains only external services.
* `.psqlrc`: This file is copied to running containers to improve the development experience when working on a Postgres session.
* `.env`: This file is read by Docker compose when it runs, and we use it to define a single environment variable with the name of the Docker compose project. By default, Docker compose takes the name from the directory, so without this environment variable, the project would be called `dockerdev`.
* `volumes` directory: This directory needs to be gitignored and its purpose is to store the contents of the postgres and minio volumes. This makes it easier to manipulate them, back them up if you are migrating to a new laptop, share them with a college, etc.
* `scripts` directory: This directory contains some utilities to aid in the setup of the environment.

Apart from the files in `.dockerdev` we have a few more moving pieces:

* `bin/dockerdev`: [All development operations](#working-with-docker) with Docker are done through this script.
* We need to add `.dockerdev/volumes/*` to the `.gitignore`.

### Working with Docker

The `bin/dockerdev` script contains all the necessary commands to start, stop and manage the services.

### Initial setup

When setting up an application for the first time, you just need to run `bin/dockerdev setup`.

This will perform several things:

* Build Docker images.
* Create the Minio bucket.

## Rails Docker for development setup

In order to Dockerize a Rails application for development, we create a `.dockerdev` directory in the application root, which contains the `Dockerfile`, `docker-compose.yml` and other support files. Separating it into its own directory avoids mixing it with the production Docker setup which usually resides in the root.

When copying the files from this repo you need to replace several values for the appropriate in your application. These values are: `<application-name>`, `<postgres-version>`, `<redis-version>`, `<ruby-version>` and `<bundler-version>`.

The `.dockerdev` directory contains the following files:

* `docker-compose.yml`: [Docker compose configuration](#docker-compose-configuration).
* `Dockerfile`: [Dockerfile used to build the image for development](#dockerfile).
* `.pryrc`: This file is copied to running containers to improve the development experience when working on Pry.
* `.psqlrc`: This file is copied to running containers to improve the development experience when working on a Postgres session.
* `com.user.docker-host-alias.plist`: This file is used to create an alias from the 127.17.0.1 to localhost, in order to [make Minio accessible from both the host and containers](#make-minio-accessible-everywhere)
* `.env`: This file is read by Docker compose when it runs, and we use it to define a single environment variable with the name of the Docker compose project. By default, Docker compose takes the name from the directory, so without this environment variable, the project would be called `dockerdev`.
* `volumes` directory: This directory needs to be gitignored and its purpose is to store the contents of the postgres and minio volumes. This makes it easier to manipulate them, back them up if you are migrating to a new laptop, share them with a college, etc.
* `scripts` directory: This directory contains some utilities to aid in the setup of the environment.

Apart from the files in `.dockerdev` we have a few more moving pieces:

* `bin/dockerdev`: [All development operations](#working-with-docker) with Docker are done through this script.
* We need to add `.dockerdev/volumes/*` to the `.gitignore`.
* [Capybara needs to be configured](#capybara-configuration) to use the selenium container.

### Working with Docker

The `bin/dockerdev` script contains all the necessary commands to start, stop and manage the application.

### Application setup

When setting up an application for the first time, you just need to run `bin/dockerdev setup`.

This will perform several things:

* Build Docker images.
* Create the Minio bucket.
* Install dependencies.
* Prepare the database.

From that point on, to install new gems or node modules you will need to do it from a container. You can open a container with bash by running `bin/dockerdev bash` and inside just run `bundle install` or `yarn install` as usual.

### Running the application

In order to have the application fully functional you need to run several processes in different terminal sessions:

* Rails server: `bin/dockerdev server`.
* Background jobs: `bin/dockerdev jobs` (only needed if you wish to run background jobs like sending e-mails).

### Rails commands

In order to execute rails command you can use `bin/dockerdev run [command]`. This will run the command inside a container. For example to run database migrations: `bin/dockerdev run rake db:migrate`.

### Other commands

* Start only services: `bin/dockerdev start-services`
* Stop and remove containers: `bin/dockerdev stop`.
* Open a bash session: `bin/dockerdev run bash`.
* Run the test suite: `bin/dockerdev run rspec`.

### Capybara configuration (Ruby on Rails only)

To get reliable test runs, we run system tests against a container that runs a pinned version of Chromium (`selenium` service in `docker-compose.yml`). This also avoids the need to have Chrome installed locally to run the test suite.

To configure Capybara to use the container add the following to `spec_helper.rb`:

```ruby
require 'socket'

LOCAL_PORT = 8200
LOCAL_IP = if ENV['SELENIUM_URL']
             Socket.ip_address_list.find(&:ipv4_private?)&.ip_address
           else
             'localhost'
           end

Capybara.register_driver :selenium_remote do |app|
    Capybara::Selenium::Driver.new(app,
                                   browser: :remote,
                                   options: Selenium::WebDriver::Chrome::Options.new,
                                   url: ENV['SELENIUM_URL'])
end

Capybara.javascript_driver = :selenium_remote

RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test

    Capybara.app_host = "http://#{LOCAL_IP}:#{LOCAL_PORT}"
    Capybara.server_host = LOCAL_IP
    Capybara.server_port = LOCAL_PORT
    Capybara.always_include_port = true
  end
end

# When using Webmock
require 'webmock/rspec'
WebMock.disable_net_connect!(
  allow_localhost: true,
  allow: [/selenium/, LOCAL_IP]
)
```

### Dockerfile

The Dockerfile used for development is pretty minimal. Below there is a description of each of its blocks for a Ruby on Rails application. For a NodeJS or Python application the Dockerfile will vary.

```docker
FROM ruby:<ruby-version>-alpine
```

We start from the ruby image which contains the full OS, basic system packages and, of course, Ruby itself.

```docker
RUN apk --update add less bash git curl wget build-base && \
    apk add postgresql-client && \
    apk add nodejs yarn && \
    apk add vim imagemagick && \
    rm -rf /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

```

This does various things:

* Install basic build tools, often needed to install other packages.
* Install a client to access the PostgreSQL database from a bash session if needed.
* Install NodeJS and Yarn.
* Install an editor (vim) and ImageMagick which is used in the majority of applications.
* Clean packages, temporary files and logs.

**NOTE:** All these operations are done in the same command to avoid caching unnecessary layers. This way the whole operation gets cached as a single layer.

```docker
ENV LANG=C.UTF-8
ENV GEM_HOME=/bundle
ENV PATH /app/bin:$GEM_HOME/bin:$GEM_HOME/gems/bin:$PATH
```

This does various things:

* Make contents of the `bin` directory of the application available to use as commands. This way when we run `rails` or `rake`, for example, inside the container, it uses the versions in `bin`.
* Tell bundler to install gems in the `/bundle` directory. This goes hand in hand with the `bundle` volume that is mounted on the containers. By keeping the gems in a known location and a volume, we persist them across containers and container restarts.

```docker
RUN gem update --system && \
    gem install bundler:$BUNDLER_VERSION
```

Install a pinned version of bundler. This is recommended to avoid the `Gemfile.lock` changing every time the image is re-built. Since the `Gemfile.lock` specifies the Bundler version used, if the image is recreated and a newer version happens to be installed, the `Gemfile.lock` will change once we install gems again.

```docker
RUN mkdir -p /app

WORKDIR /app
```

Create the directory where the app volume will be mounted and tell it to work from this directory.

### Docker compose configuration

The Docker compose setup contains services for both the application and infrastructure.

All the application services inherit from a common `app` and/or `backend` configuration, whose more interesting aspects are:

* All the infrastructure/running related environment variables are specified in this configuration. Application specific variables should be defined using another mechanism (like `dotenv`).
* It uses volumes for various things:
  * `../:/app:cached`: This makes the whole application available as a volume, so that changes to the codebase are propagated to the container.
  * `rails_cache`: Keeping the cache in a volume persists it across containers restarts, improving the performance of the application during development.
  * `bundle`: The image is configured to install gems in the `/bundle` directory of the container, therefore we need to make this directory available.
  * `./.psqlrc:/root/.psqlrc:ro` and `./.pryrc:/root/.pryrc:ro`: This is a simple way of copying files to the container without needing to embed them in the image.

The rest of services defined are pretty much self-explanatory.

### Make Minio accessible everywhere

When working with MacOS we need to run `sudo bin/dockerdev setup-localhost-alias` the first time we are setting up the project.

This sets up an alias of 172.17.0.1 to localhost in order to be able to interact with Minio.

#### Why?

Minio needs to be accessible both from the browser and from the containers. We need it from the browser in order to access files from pages (like images) and we need it from the container in order to upload files.

However, there is no way to access Minio from both places using the same URL:

* From the host, we can access by using `localhost`: <http://localhost:9000>
* From the container, we can access by using the name of the container: <http://minio:9000>

To overcome this limitation, we make use of the fact that from the container the IP `127.17.0.1` can be used to access the host. By setting up an alias on the host from this IP to localhost we can use the URL <http://127.17.0.1:9000> from both the host and the container:

* From the host, it just maps to localhost, so it's the same as before.
* From the container, it maps to the host, and from there it accesses Minio through the exposed port.


# React guidelines

We bootstrap React applications with [Next.js](https://nextjs.org/) (App Router) by default. Use [Vite](https://vite.dev/) for single-page apps that don't need server-side rendering, SEO or server code. [Create React App](https://react.dev/blog/2025/02/14/sunsetting-create-react-app), which we used on older projects, was deprecated by the React team in 2025 and must not be used for new projects. For mobile apps, see our [React Native guidelines](/our-development-guides/react-native-guidelines), which build on this guide.

* 1. [Do's and Don'ts](#1-dos-and-donts)
  * 1.1. [Use TypeScript](#11-use-typescript)
  * 1.2. [Use a generator to bootstrap the project](#12-use-a-generator-to-bootstrap-the-project)
  * 1.3. [Write function components](#13-write-function-components)
  * 1.4. [Use hooks for state management](#14-use-hooks-for-state-management)
  * 1.5. [Use a declarative API library](#15-use-a-declarative-api-library)
  * 1.6. [Do use function declarations](#16-do-use-function-declarations)
  * 1.7. [Do name exports](#17-do-name-exports)
  * 1.8. [Do name prop types](#18-do-name-prop-types)
  * 1.9. [Lint and format with ESLint](#19-lint-and-format-with-eslint)
  * 1.10. [Make substantial compositions their own component](#110-make-substantial-compositions-their-own-component)
* 2. [General project organization and architecture](#2-general-project-organization-and-architecture)
  * 2.1. [Next.js project structure](#21-nextjs-project-structure)
  * 2.2. [Vite SPA project structure](#22-vite-spa-project-structure)
  * 2.3. [References (project structure)](#23-references-project-structure)
* 3. [Description of the most common patterns used to solve common problems](#3-description-of-the-most-common-patterns-used-to-solve-common-problems)
  * 3.1. [State management](#31-state-management)
    * 3.1.1. [Local state management](#311-local-state-management)
    * 3.1.2. [Global state management](#312-global-state-management)
  * 3.2. [External services](#32-external-services)
  * 3.3. [GraphQL](#33-graphql)
  * 3.4. [REST](#34-rest)
  * 3.5. [Routing](#35-routing)
    * 3.5.1. [Next.js App Router](#351-nextjs-app-router)
    * 3.5.2. [Route definitions](#352-route-definitions)
    * 3.5.3. [Access route parameters](#353-access-route-parameters)
    * 3.5.4. [SPA with Vite and react-router](#354-spa-with-vite-and-react-router)
  * 3.6. [Server and client components](#36-server-and-client-components)
  * 3.7. [Testing](#37-testing)
* 4. [Libraries](#4-libraries)
  * 4.1. [Recommended libraries](#41-recommended-libraries)
  * 4.2. [Other libraries we have used](#42-other-libraries-we-have-used)
  * 4.3. [Libraries worth taking a look into](#43-libraries-worth-taking-a-look-into)
* 5. [Learning resources](#5-learning-resources)

## 1. Do's and Don'ts

### 1.1. Use TypeScript

Always. Start from the TypeScript template of your framework and follow our [TypeScript guidelines](/our-development-guides/typescript-guidelines). The rules below (1.6 to 1.8) complement that guide with React-specific conventions.

### 1.2. Use a generator to bootstrap the project

A project generator saves a lot of boilerplate work and provides common conventions.

* Default: [Next.js](https://nextjs.org/) with the App Router: `npx create-next-app@latest` (pick TypeScript, ESLint, App Router and the `src/` directory).
* Single-page apps without SSR, SEO or server code: [Vite](https://vite.dev/): `npm create vite@latest -- --template react-ts`.

Older projects were bootstrapped with Create React App, but it's deprecated and must not be used for new projects.

### 1.3. Write function components

Functions and hooks are the standard. Class components are legacy: never write new ones, and migrate them when you touch them.

### 1.4. Use hooks for state management

* In general, prefer React's hooks for local state management
* For global state, prefer server-side state: keep the server as the source of truth and read it with Server Components or TanStack Query
* Keep shareable UI state (pagination, filters, search, sort, active tab) in the URL search params, so a link reproduces the exact view
* If server-side state is not possible, prefer React Context for simple state (theme, current user, feature flags)
* For more complex client-side state, use a library (with hooks), for example [zustand](https://github.com/pmndrs/zustand)
* Avoid redux. If a project already depends on it or it's unavoidable, use [Redux Toolkit](https://redux-toolkit.js.org/) and its hooks API

See patterns (below) for examples and usages.

### 1.5. Use a declarative API library

It reduces boilerplate code a lot.

We currently use:

* REST: [TanStack Query](https://tanstack.com/query) (formerly react-query)
* GraphQL: [apollo-client](https://github.com/apollographql/apollo-client)

Don't write API types by hand. For external REST APIs, generate the types and the client from their OpenAPI spec with [Hey API](https://heyapi.dev/) (`@hey-api/openapi-ts`). See [REST](#34-rest).

In Next.js, Server Components fetch data directly and don't need a client library. TanStack Query is for client components. See [Server and client components](#36-server-and-client-components).

### 1.6. Do use function declarations

For a better readability.

```tsx
// Don't declare arrow functions
const App = () => (
  <div>
    <Logo />
  </div>
);

// DO declare functions
function App() {
  return (
    <div>
      <Logo />
    </div>
  );
}
```

### 1.7. Do name exports

It allows to export multiple values and it encourages the use of the same naming.

```tsx
export function LoginPage() {
  ...
}
```

The exception is Next.js file conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx` and `not-found.tsx` require a default export, and route handlers export named `GET`, `POST`, etc. Use default exports only there.

### 1.8. Do name prop types

Declare an exported `XProps` type right above the component and use it in the signature. Don't use `React.FC` or anonymous object types in the signature. This follows the [Use named types](/our-development-guides/typescript-guidelines#use-named-types-avoid-anonymous-types) and "Export every type" rules from our TypeScript guidelines.

```tsx
export type LoginFormProps = { user?: string };

export function LoginForm({ user = "" }: LoginFormProps) {
  ...
}
```

### 1.9. Lint and format with ESLint

* Use [eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks) (`rules-of-hooks`, `exhaustive-deps`; version 6 and later also ships the React Compiler rules). `create-next-app` includes it through `eslint-config-next`. On Vite projects, add it to the ESLint config yourself.
* Format with [ESLint Stylistic](https://eslint.style/) (`@stylistic/eslint-plugin`) instead of Prettier. One tool, one config, one `--fix` pass: formatting rules live next to the rest of the lint rules and there is no conflict between two formatters.

### 1.10. Make substantial compositions their own component

A composition of components becomes a named component as soon as it has enough substance, even if it is used only once. Signs of substance: it represents a concept you can name (`UserCard`, `InvoiceSummary`), it has its own state, handlers or data needs, or it is more than a few lines of nested JSX. Give it a name, place it at the nearest common ancestor of its consumers (generic UI primitives go straight to `src/components/`, see [3.5.1](#351-nextjs-app-router)), type its props (see 1.8) and treat it like any other component: it gets its own file, its own tests and its own review.

Repetition is the second trigger: the same combination of components, wiring and props in more than one place is a component by definition. Don't copy-paste the JSX. Duplicated compositions drift apart, and every visual or behavioural change has to be hunted down in each copy.

```tsx
// Don't leave a substantial composition inline in the page, even if it appears only here
export default function ProfilePage({ user }: ProfilePageProps) {
  return (
    <main>
      <Card>
        <CardHeader>
          <Avatar src={user.avatarUrl} alt={user.name} />
          <CardTitle>{user.name}</CardTitle>
          <Badge>{user.role}</Badge>
        </CardHeader>
        <CardContent>{user.bio}</CardContent>
      </Card>
    </main>
  );
}

// DO name it and pass in only what varies
export type UserCardProps = { user: User };

export function UserCard({ user }: UserCardProps) {
  return (
    <Card>
      <CardHeader>
        <Avatar src={user.avatarUrl} alt={user.name} />
        <CardTitle>{user.name}</CardTitle>
        <Badge>{user.role}</Badge>
      </CardHeader>
      <CardContent>{user.bio}</CardContent>
    </Card>
  );
}

export default function ProfilePage({ user }: ProfilePageProps) {
  return (
    <main>
      <UserCard user={user} />
    </main>
  );
}
```

Small one-off layout fragments with no name of their own stay inline.

## 2. General project organization and architecture

We follow a conventional `src/` folder structure. Shared conventions, regardless of the framework:

* One config file: `src/config.ts`
* Declare all app route paths at `src/routes.ts` (see [Route definitions](#352-route-definitions))
* All non-route components under `components/` (can be nested)
* All hooks under `hooks/` (can expose Providers)
* One folder for each (external) service. For example `api/` (for REST APIs), `graphql/` for GraphQL or `auth/` for authorization service. They can include type definitions, data transformations, clients or anything related to that service and communication with it.
* One folder for locales: `locales/`
* The rest: utility functions, helpers, etc... under `lib/` (keep it clean, please)

Routing is where the two flavours differ: Next.js uses the `app/` folder, Vite SPAs use `pages/` plus a `Router.tsx`.

### 2.1. Next.js project structure

```
src/
|- app/                   # App Router: folders are URL segments, files are UI
|  |- layout.tsx          # root layout (html, body, providers)
|  |- page.tsx            # /
|  |- posts/
|  |  |- _components/     # segment-private UI (not routable, see 3.5.1)
|  |  |- _lib/            # segment-private actions/helpers (not routable)
|  |  |- page.tsx         # /posts
|  |  |- [id]/
|  |     |- page.tsx      # /posts/:id (dynamic segment)
|  |     |- loading.tsx   # streaming fallback (optional)
|  |- (admin)/            # route group: shared layout, adds no URL segment
|  |  |- layout.tsx
|  |  |- users/
|  |     |- page.tsx      # /users
|  |- api/                # Route Handlers (optional)
|     |- health/
|        |- route.ts
|- routes.ts              # typed route helpers (see 3.5.2)
|- config.ts              # application configuration
|- components/            # non-route components ("use client" only when needed)
|  |- ui/                 # component library primitives (shadcn/ui CLI output, see 4.1)
|  |  |- button.tsx
|  |  |- dialog.tsx
|  |- Spinner.tsx         # shared component
|  |- posts/              # folder for specific areas or sections
|     |- PostForm.tsx
|- hooks/
|  |- useUser.ts          # hook
|- locales/
|  |- type.d.ts           # Locale type definitions
|  |- en-GB.ts            # locale for en-GB
|- api/                   # REST client (optional, see 3.4)
|  |- client.ts           # client configuration (base URL, auth)
|  |- generated/          # Hey API output, do not edit
|- graphql/               # GraphQL folder (optional)
|  |- types.d.ts
|  |- schema.ts
|- auth/                  # Authorization service (optional)
|  |- types.d.ts
|  |- index.ts
|- lib/                   # internal libraries aka "Everything else"
   |- randomColor.ts
next-env.d.ts             # generated by Next.js at the project root, do not edit
```

There is no `pages/` folder: the Pages Router is legacy. Only Next.js special files (`page.tsx`, `layout.tsx`, `route.ts`, etc.) sit directly in a segment folder. Segment-scoped code is colocated in private folders (`_components/`, `_lib/`, see [3.5.1](#351-nextjs-app-router)); generic UI components and anything shared across unrelated areas go to `components/`. Component library primitives live in `components/ui/`: it's where the shadcn/ui CLI installs them, and we keep the same folder with other component libraries so the split between library primitives and our own components is always the same.

### 2.2. Vite SPA project structure

```
src/
|- main.tsx               # entry point (createRoot)
|- App.tsx                # Application setup (providers)
|- Router.tsx             # BrowserRouter and Routes (see 3.5.4)
|- routes.ts              # typed route helpers (see 3.5.2)
|- config.ts              # application configuration
|- pages/                 # page components, mimic the URL hierarchy
|  |- HomePage.tsx
|  |- PostListPage.tsx
|  |- PostPage.tsx
|  |- admin/
|     |- AdminUserListPage.tsx
|- components/            # same as Next.js
|- hooks/
|- locales/
|- api/                   # same as Next.js (optional)
|- graphql/
|- auth/
|- lib/
|- vite-env.d.ts          # Vite client types (from the template)
```

If using a repo for both api and client, put the above inside `client/` folder

### 2.3. References (project structure)

* Next.js [project structure](https://nextjs.org/docs/app/getting-started/project-structure) docs
* Route definitions idea taken from [Redwood](https://github.com/redwoodjs/redwood) framework

## 3. Description of the most common patterns used to solve common problems

### 3.1. State management

* In general, prefer hooks over any other solution
* Most "global state" is really server state. Keep it on the server and let the data layer cache it instead of copying it into a client store
* If a user could want to share, bookmark or reload a view, its state belongs in the URL, not in a store
* You probably don't need redux. Hooks, Context and zustand cover almost every case. If redux is unavoidable, use Redux Toolkit and its hooks API

#### 3.1.1. Local state management

React hooks (`useState`, `useReducer`) are enough most of the time. Keep state as close as possible to the components that use it and lift it only when needed.

#### 3.1.2. Global state management

Pick the simplest option that works, in this order:

1. **Server-side state.** Data that lives in a database or API belongs to the server. Read it in Server Components (Next.js) or with [TanStack Query](https://tanstack.com/query) in client components, and mutate it with Server Actions or mutations. TanStack Query's cache is the "global store" for that data: don't duplicate it in a client store
2. **URL state.** Anything that describes *which* view the user is looking at goes in the URL: current page, page size, filters, search query, sort column and direction, active tab, open drawer or selected item. Links become shareable, the back button works, reloads keep the view and Server Components can read the params on the server. See [URL state](#313-url-state)
3. **React Context.** For simple client-only state that rarely changes and is read by many components: theme, locale, current user, feature flags. Keep each context small and colocate the provider with the subtree that needs it
4. **zustand.** For complex client-only state: many writers, frequent updates, derived data or state that outlives a subtree (multi-step wizards, editors, carts). Prefer [zustand](https://github.com/pmndrs/zustand) over hand-rolled context plus reducers. Keep stores small and split them by domain
5. **redux.** Only when a project already depends on it. Use Redux Toolkit and its hooks API

Context is not a global store: every consumer re-renders when the value changes. If a context grows or updates often, move it to zustand.

#### 3.1.3. URL state

The URL is the first place to put view state. Rules:

* Search params hold *view* state (`?page=2&q=react&sort=-createdAt&status=open`); the path holds *identity* (`/posts/42`). Never put secrets or large payloads in either
* Parse and validate search params with [zod](https://zod.dev/) in one place per route (`_lib/searchParams.ts` in Next.js). Defaults live in the schema, not spread over the components
* Omit a param when it has its default value so canonical URLs stay short and cache-friendly
* Reset dependent params together: changing a filter or the search query sends the user back to page 1
* Read the params on the server whenever possible. In Next.js, `page.tsx` receives `searchParams` (a Promise since Next.js 15): validate them and fetch in the Server Component. Client components that need to update them use `useSearchParams` with `router.replace` (or `push` when the change should create a history entry)
* In a Vite SPA use react-router's `useSearchParams` plus the same zod schema
* If the project handles many params, use [nuqs](https://nuqs.dev/): typed, `useState`-like search params with a shared parser for server and client. It works on Next.js App Router and react-router

```ts
// src/app/(dashboard)/posts/_lib/searchParams.ts
import { z } from "zod";

export const postsSearchParamsSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  q: z.string().trim().default(""),
  status: z.enum(["open", "closed", "all"]).default("all"),
  sort: z.enum(["createdAt", "-createdAt", "title"]).default("-createdAt"),
});

export type PostsSearchParams = z.infer<typeof postsSearchParamsSchema>;
export const defaults = postsSearchParamsSchema.parse({});
```

```tsx
// src/app/(dashboard)/posts/page.tsx
export type PostListPageProps = {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
};

export default async function PostListPage({ searchParams }: PostListPageProps) {
  const params = postsSearchParamsSchema.parse(await searchParams);
  const posts = await getPosts(params);
  return <PostList posts={posts} params={params} />;
}
```

```tsx
// src/app/(dashboard)/posts/_components/PostFilters.tsx
"use client";

import { usePathname, useRouter, useSearchParams } from "next/navigation";

export function PostFilters({ params }: { params: PostsSearchParams }) {
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();

  function update(patch: Partial<PostsSearchParams>) {
    const next = new URLSearchParams(searchParams);
    // any filter change resets pagination; default values are dropped from the URL
    for (const [key, value] of Object.entries({ ...patch, page: 1 })) {
      value === defaults[key as keyof PostsSearchParams]
        ? next.delete(key)
        : next.set(key, String(value));
    }
    router.replace(`${pathname}?${next}`);
  }

  return (
    <input
      defaultValue={params.q}
      onChange={(e) => update({ q: e.target.value })}
      placeholder="Search posts"
    />
  );
}
```

### 3.2. External services

* Create a clean interface for each service. For example: `src/api/index.ts` (functions to send http requests to a REST API) or `src/graphql/index.ts` (queries and mutations of a GraphQL endpoint)
* Prefer generated types over hand-written ones: Hey API for REST, GraphQL Code Generator for GraphQL. Fall back to `<service-name>/types.ts` when there is no schema to generate from
* Use declarative data fetching: prefer `useQuery` over `fetch` (available both in Apollo client and TanStack Query)

### 3.3. GraphQL

We currently use [apollo-client](https://www.apollographql.com/docs/react/)

Generate types and code as much as possible with [GraphQL Code Generator](https://the-guild.dev/graphql/codegen).

In general, keep all graphql related code inside `graphql/` folder.

### 3.4. REST

* Generate the client and its types from the OpenAPI spec with [Hey API](https://heyapi.dev/) (`@hey-api/openapi-ts`) into `src/api/generated/`. Its TanStack Query plugin produces ready-to-use query and mutation options. See our [TypeScript guidelines](/our-development-guides/typescript-guidelines)
* Hand-write only `src/api/client.ts` (base URL, auth headers, interceptors) and thin wrappers around the generated code
* No spec? Fall back to a single `src/api/index.ts` exporting all API interactions and `src/api/types.ts` for entity type definitions
* If Auth and API are different services, is common to have two folders (`src/auth` and `src/api`) and the API depends on authorization (JWT tokens, for example). If auth and API are in the same service, the `src/auth` folder can be omitted.

### 3.5. Routing

Next.js routes are defined by the file system (App Router). Vite SPAs use react-router. In both cases keep the route paths in `src/routes.ts` (see [Route definitions](#352-route-definitions)).

#### 3.5.1. Next.js App Router

* Folders under `app/` are URL segments. `page.tsx` is the route UI, `layout.tsx` wraps its children and persists across navigation
* `[id]` for dynamic segments, `[...slug]` for catch-all routes, `(group)` for route groups (shared layout, no URL segment)
* `loading.tsx`, `error.tsx` and `not-found.tsx` are the Suspense, error boundary and 404 conventions
* Navigate with `next/link`
* Only Next.js special files (`page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`, `route.ts`) sit directly in a segment folder. Cross-cutting reusable components still go to `components/`; code that belongs to one route segment is colocated inside the segment using **private folders**.

**Private folders.** Prefixing a folder with an underscore (`_folder`) opts it and all its children out of routing: the router never turns it into a URL segment, even if it contains files named like special files. Use them to colocate segment-scoped code without polluting the segment root:

* `_components/`: UI components used only by that segment.
* `_lib/`: logic: Server Actions, validation, search params parsing (see [3.1.3](#313-url-state)), helpers.
* Tests stay next to their source inside the same private folder.

```
app/
|- (dashboard)/
|  |- _components/        # shared by every dashboard route
|  |  |- AppHeader.tsx
|  |- _lib/
|  |  |- searchParams.ts
|  |- layout.tsx
|  |- posts/
|     |- _components/
|     |  |- PostRow.tsx
|     |- _lib/
|     |  |- actions.ts    # Server Actions for this segment
|     |- page.tsx
```

Place shared code at the **nearest common ancestor** of its consumers: used by one route, that segment's private folders; used by several routes under a layout, the private folders of the segment that owns the layout; used across unrelated areas, `src/components/` and `src/lib/` as before. Exception: generic, purely UI components with no domain knowledge (a combobox, a modal, a button) skip this rule and always live in the root `src/components/`, even if only one segment uses them today. They are the project's design system, not segment code. Two notes: colocation in `app/` is safe even without the underscore (only `page.tsx` and `route.ts` create URLs), so the prefix's value is signalling intent and protecting against future special-file collisions; and if a real URL segment must start with an underscore, name the folder `%5FfolderName`. Reference: Next.js docs, [Project structure: private folders](https://nextjs.org/docs/app/getting-started/project-structure#private-folders).

```tsx
// src/app/posts/[id]/page.tsx (Server Component; default export required by Next.js)
// params is a Promise since Next.js 15
export type PostPageProps = { params: Promise<{ id: string }> };

export default async function PostPage({ params }: PostPageProps) {
  const { id } = await params;
  const post = await getPost(id);
  return <PostDetail post={post} />;
}
```

#### 3.5.2. Route definitions

It's a file to generate route paths. Advantages:

* You get an overview of all available routes on the app
* It helps to prevent errors when declaring routes
* Route completion via editor
* In Next.js it keeps links in sync with the `app/` folder structure

```ts
export default {
  posts: () => `/posts`,
  post: (id: string) => `/posts/${id}`,
  admin: {
    users: () => `/admin/users`,
  },
};
```

Usage:

```tsx
import routes from "./routes";

// Next.js
<Link href={routes.post(post.id)}>Read more</Link>;

// react-router
<Link to={routes.post(post.id)}>Read more</Link>;
```

#### 3.5.3. Access route parameters

For a route like `/posts/:postId/comments/:commentId`:

```tsx
// Next.js Server Component: read the params prop (see 3.5.1)
const { postId, commentId } = await params;

// Next.js client component
"use client";
import { useParams } from "next/navigation";
const { postId, commentId } = useParams<{ postId: string; commentId: string }>();

// react-router
import { useParams } from "react-router";
const { postId, commentId } = useParams();
```

#### 3.5.4. SPA with Vite and react-router

* Use [react-router](https://reactrouter.com/) v7 with hooks. Install the `react-router` package: `react-router-dom` is now a thin re-export kept for compatibility
* A page is a component rendered from `Router.tsx`. It can access route parameters and lives in `src/pages/` (nested to reflect the URL structure)
* react-router v7 also has a *framework mode* (loaders, actions, SSR) that is the successor of Remix. It's a valid option for projects that don't fit Next.js

```tsx
// src/Router.tsx
import { BrowserRouter, Routes, Route } from "react-router";
import routes from "./routes";

export function Router() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path={routes.posts()} element={<PostListPage />} />
        <Route path={routes.post(":id")} element={<PostPage />} />
        <Route path={routes.admin.users()} element={<AdminUserListPage />} />
      </Routes>
    </BrowserRouter>
  );
}
```

### 3.6. Server and client components

Only applies to Next.js. In a Vite SPA every component is a client component.

* Every component under `app/` is a Server Component by default: it can be `async`, fetches data directly (database via Drizzle, API client) and has no hooks or event handlers
* Add `"use client"` only to the leaves that need state, effects or browser APIs. Keep the boundary as low in the tree as possible
* Data: Server Components fetch directly, client components use TanStack Query
* Mutations: Server Actions (`"use server"`) with `useActionState`. Keep react-hook-form for client-side validation UX
* React 19: `use()` reads promises and context, `ref` is a normal prop (no `forwardRef`), `<Context value={...}>` renders as a provider
* React Compiler: enable it (`reactCompiler: true` in `next.config.ts`, `babel-plugin-react-compiler` on Vite) and stop hand-writing `useMemo`, `useCallback` and `memo` unless profiling shows a need

### 3.7. Testing

* Unit and component tests: [Vitest](https://vitest.dev/) with [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/). Async Server Components are not supported by React Testing Library yet: cover them with end-to-end tests
* End-to-end tests: [Playwright](https://playwright.dev/)
* Favour end-to-end testing over component tests
* More important to cover critical paths than general coverage
* Ensure business logic are pure functions and write unit tests for them when needed

See also our [Testing guidelines](/our-development-guides/testing-guidelines).

## 4. Libraries

### 4.1. Recommended libraries

* Framework: [Next.js](https://nextjs.org/) (default), [Vite](https://vite.dev/) for SPAs
* Components: [shadcn/ui](https://ui.shadcn.com/) (our default choice; copies the components into `components/ui/`)
* Styling: [tailwindcss](https://tailwindcss.com/) (used by default in all our frontend projects)
* Internationalization: [react-intl](https://formatjs.github.io/docs/react-intl/), or [next-intl](https://next-intl.dev/) on Next.js App Router
* Forms: [react-hook-form](https://react-hook-form.com/)
* Validation: [zod](https://zod.dev/) (with `@hookform/resolvers` for forms)
* Global state management: [zustand](https://github.com/pmndrs/zustand)
* Server state / data fetching: [TanStack Query](https://tanstack.com/query) (formerly react-query)
* Tables: [TanStack Table](https://tanstack.com/table) (headless; pair it with shadcn/ui's `Table` or the `Data Table` recipe)
* Typed REST client: [Hey API](https://heyapi.dev/)
* Http: [ky](https://github.com/sindresorhus/ky)
* Utilities: [es-toolkit](https://es-toolkit.dev/) (modern, tree-shakeable lodash replacement; prefer it over lodash and over hand-written helpers in `lib/`)
* GraphQL API: [apollo-client](https://www.apollographql.com/docs/react/)
* Routing: Next.js App Router, or [react-router](https://reactrouter.com/) v7 for SPAs
* Testing: [Vitest](https://vitest.dev/), [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), [Playwright](https://playwright.dev/)
* Lint and format: [eslint-plugin-react-hooks](https://react.dev/reference/eslint-plugin-react-hooks), [ESLint Stylistic](https://eslint.style/)

### 4.2. Other libraries we have used

* Components (we prefer shadcn/ui, see 4.1; these appear in existing projects or when a client requires them)
  * [antd](https://ant.design/docs/react/introduce)
  * [PrimeReact](https://primereact.org/)
* Hooks:
  * [react-use](https://github.com/streamich/react-use)

### 4.3. Libraries worth taking a look into

* State management
  * [jotai](https://github.com/pmndrs/jotai)
* Component library
  * [Chakra UI](https://chakra-ui.com/)

## 5. Learning resources

* React docs are quite good. Recommended reading: <https://react.dev/learn>
* Next.js official course: <https://nextjs.org/learn>
* egghead.io is one of our favourite places to learn and Kent C. Dodds is a master, so this can't fail: <https://egghead.io/courses/the-beginner-s-guide-to-react>


# React Native guidelines

We build mobile apps with [React Native](https://reactnative.dev/) through [Expo](https://expo.dev/) (managed workflow, Expo Router, New Architecture, React Compiler). This guide only covers what is specific to React Native and Expo. Everything else (TypeScript, function components, named exports, prop types, composition, state management, data fetching, ESLint) is the same as in our [React guidelines](/our-development-guides/react-guidelines): read that guide first and come back here for the mobile-specific parts.

* 1. [Do's and Don'ts](#1-dos-and-donts)
  * 1.1. [Follow the React guidelines](#11-follow-the-react-guidelines)
  * 1.2. [Use Expo, managed workflow](#12-use-expo-managed-workflow)
  * 1.3. [Never touch the native folders](#13-never-touch-the-native-folders)
  * 1.4. [Install native packages with expo install](#14-install-native-packages-with-expo-install)
  * 1.5. [Read the versioned Expo docs](#15-read-the-versioned-expo-docs)
  * 1.6. [Only route files live in app/](#16-only-route-files-live-in-app)
  * 1.7. [Style with NativeWind, not StyleSheet](#17-style-with-nativewind-not-stylesheet)
  * 1.8. [Translate every user-facing string](#18-translate-every-user-facing-string)
  * 1.9. [Use kebab-case file names](#19-use-kebab-case-file-names)
  * 1.10. [Lint with expo lint and ESLint Stylistic](#110-lint-with-expo-lint-and-eslint-stylistic)
* 2. [Project organization and architecture](#2-project-organization-and-architecture)
  * 2.1. [Expo project structure](#21-expo-project-structure)
  * 2.2. [Components, hooks and domain folders](#22-components-hooks-and-domain-folders)
  * 2.3. [Path aliases](#23-path-aliases)
  * 2.4. [Tooling](#24-tooling)
* 3. [Common patterns](#3-common-patterns)
  * 3.1. [Routing with Expo Router](#31-routing-with-expo-router)
  * 3.2. [Components and platform APIs](#32-components-and-platform-apis)
  * 3.3. [Styling and theming](#33-styling-and-theming)
  * 3.4. [Server state with TanStack Query](#34-server-state-with-tanstack-query)
  * 3.5. [REST API client](#35-rest-api-client)
  * 3.6. [Forms](#36-forms)
  * 3.7. [Internationalization](#37-internationalization)
  * 3.8. [Configuration and environment](#38-configuration-and-environment)
  * 3.9. [Testing](#39-testing)
  * 3.10. [Authentication and secure storage](#310-authentication-and-secure-storage)
  * 3.11. [Builds and releases with EAS](#311-builds-and-releases-with-eas)
  * 3.12. [Third-party SDKs](#312-third-party-sdks)
* 4. [Libraries](#4-libraries)
  * 4.1. [Recommended libraries](#41-recommended-libraries)
  * 4.2. [Other libraries we have used](#42-other-libraries-we-have-used)
  * 4.3. [Libraries worth taking a look into](#43-libraries-worth-taking-a-look-into)
* 5. [Learning resources](#5-learning-resources)

## 1. Do's and Don'ts

### 1.1. Follow the React guidelines

Everything in the [React guidelines](/our-development-guides/react-guidelines) applies unless this guide says otherwise. In particular:

* [Use TypeScript](/our-development-guides/react-guidelines#11-use-typescript) and the [TypeScript guidelines](/our-development-guides/typescript-guidelines)
* [Write function components](/our-development-guides/react-guidelines#13-write-function-components), [use function declarations](/our-development-guides/react-guidelines#16-do-use-function-declarations), [name exports](/our-development-guides/react-guidelines#17-do-name-exports) and [name prop types](/our-development-guides/react-guidelines#18-do-name-prop-types)
* [Make substantial compositions their own component](/our-development-guides/react-guidelines#110-make-substantial-compositions-their-own-component)
* [State management](/our-development-guides/react-guidelines#31-state-management): hooks for local state, TanStack Query for server state, Context for simple shared state, zustand for complex client state, no redux
* [External services](/our-development-guides/react-guidelines#32-external-services) and [REST](/our-development-guides/react-guidelines#34-rest): generated clients over hand-written ones
* React 19 and React Compiler: no hand-written `useMemo`, `useCallback` or `memo` (see [Server and client components](/our-development-guides/react-guidelines#36-server-and-client-components); the Server Components part does not apply, every React Native component is a client component)

The exception to the named exports rule is Expo Router: route files in `src/app/` require a default export, the same way Next.js special files do.

### 1.2. Use Expo, managed workflow

Always start from Expo: `bun create expo-app@latest` (the default template ships TypeScript, Expo Router and the `src/` folder question). Bare React Native (`react-native init`, now `@react-native-community/cli`) is only for projects that already exist in that shape or that need native code Expo cannot express with a config plugin, which is rare. We use [bun](https://bun.sh/) as the package and script manager: `bun install`, `bun add`, `bun run <script>` and `bunx <cli>` instead of their npm counterparts.

Expo gives us file-based routing, a versioned SDK that keeps native modules compatible, over-the-air updates, EAS builds and a dev client, and it lets the team ship without opening Xcode or Android Studio.

### 1.3. Never touch the native folders

The `android/` and `ios/` folders are generated by prebuild and are in `.gitignore`. Never create, edit or commit them. All native configuration goes through `app.json` (or `app.config.ts`) and [config plugins](https://docs.expo.dev/config-plugins/introduction/): icons, splash screen, permissions, deep link scheme, bundle identifiers, orientation.

If a library asks you to edit `AndroidManifest.xml`, `Info.plist` or a Gradle file, look for its config plugin first: the library itself, then the community [`@config-plugins/*`](https://github.com/expo/config-plugins) packages. Write your own config plugin if there is none. Build settings that Expo does not expose (iOS deployment target, Kotlin version, ProGuard) go through [expo-build-properties](https://docs.expo.dev/versions/latest/sdk/build-properties/), not through the native folders. Ejecting to a bare workflow is a team decision, not a shortcut.

### 1.4. Install native packages with expo install

Any package that contains native code goes in with `bunx expo install <pkg>`, so Expo picks the version compatible with the current SDK. Pure JavaScript packages go in with `bun add` as usual.

After adding, removing or upgrading dependencies run `bunx expo-doctor`, and fix version drift with `bunx expo install --fix`.

When a package must stay pinned outside the SDK's recommended version (a Reanimated major the app is not ready for), declare it in `package.json` under `expo.install.exclude` and say why in a comment next to the dependency or in `CLAUDE.md`. The same goes for `expo.doctor.reactNativeDirectoryCheck.exclude`. A pin without a reason gets "fixed" by the next `expo install --fix`.

### 1.5. Read the versioned Expo docs

Expo changes a lot between SDKs and the training data of AI assistants is usually behind. Before using an unfamiliar API, read the docs for the exact SDK the project uses (for example `https://docs.expo.dev/versions/v57.0.0/`), never the "latest" page. Put that URL in the project's `CLAUDE.md` so assistants read it too.

### 1.6. Only route files live in app/

`src/app/` is Expo Router's territory: every file there is a screen or a layout. Keep those files thin. They read route params, and render a screen component from `src/components/<feature>/`. Components, hooks, queries and types live in their own top-level folders (see [2](#2-project-organization-and-architecture)) and are imported into the route file.

```tsx
// src/app/(tabs)/posts/[id].tsx: route file, default export required by Expo Router
import { useLocalSearchParams } from "expo-router";

import { PostDetail } from "@/components/posts/post-detail/post-detail";

export default function PostScreen() {
  const { id } = useLocalSearchParams<{ id: string }>();

  return <PostDetail id={id} />;
}
```

This is the mobile equivalent of the "only Next.js special files sit directly in a segment folder" rule from the React guide. A route file that grows `useState`, data hooks or JSX beyond a wrapper is a sign that a screen component is missing: move that code to `src/components/<feature>/` and leave the route file as a one-liner.

### 1.7. Style with NativeWind, not StyleSheet

Style exclusively with [NativeWind](https://www.nativewind.dev/) `className` utilities on top of the [gluestack-ui](https://gluestack.io/) primitives (see [3.3](#33-styling-and-theming)). No `StyleSheet.create`, no inline `style` objects, except for values that cannot be expressed as utilities: Reanimated animated styles, color props of native components (`SymbolView` tints, native tab colors) and third-party components that only accept `style`.

Theme tokens (colors, spacing, fonts) live in `global.css` following the Tailwind v4 architecture. Don't duplicate them in a `tailwind.config.js` theme block or in JavaScript constants.

### 1.8. Translate every user-facing string

All copy goes through [react-i18next](https://react.i18next.com/), from day one, even when the app ships in one language. Hardcoded user-visible strings in components are forbidden. English is the source language. See [3.7](#37-internationalization).

### 1.9. Use kebab-case file names

Expo's template uses kebab-case for every file (`post-detail.tsx`, `use-color-scheme.ts`, `_layout.tsx`), and file-based routing turns file names into URL segments. Follow it across the whole project, components included, instead of the PascalCase file names of our web projects. Component and hook identifiers stay PascalCase and camelCase as usual.

### 1.10. Lint with expo lint and ESLint Stylistic

Use `eslint-config-expo/flat` as the base (it includes `eslint-plugin-react-hooks` and the Expo rules) plus [ESLint Stylistic](https://eslint.style/) for formatting, exactly as in the [React guidelines](/our-development-guides/react-guidelines#19-lint-and-format-with-eslint). No Prettier. Run it through `bun run lint` (`expo lint`), which wires the Expo defaults. Ignore `src/lib/api/generated/**`, `android/**`, `.expo/**` and `coverage/**`.

```js
// eslint.config.js
const { defineConfig } = require("eslint/config");
const expoConfig = require("eslint-config-expo/flat");
const stylistic = require("@stylistic/eslint-plugin");

module.exports = defineConfig([
  expoConfig,
  stylistic.configs.customize({ semi: true, arrowParens: true, braceStyle: "1tbs" }),
  { ignores: ["dist/*", "coverage/**", "android/**", ".expo/**", "src/lib/api/generated/**"] },
]);
```

Add three more plugins on top of the Expo config: [@tanstack/eslint-plugin-query](https://tanstack.com/query/latest/docs/eslint/eslint-plugin-query) (catches wrong query keys and unstable deps), [eslint-plugin-react-you-might-not-need-an-effect](https://github.com/NickvanDyke/eslint-plugin-react-you-might-not-need-an-effect) (flags effects that derive state or sync props) and [eslint-plugin-simple-import-sort](https://github.com/lydell/eslint-plugin-simple-import-sort) (one import order, auto-fixed).

Generated gluestack components under `src/components/ui/` are linted like any other code. They are ours.

## 2. Project organization and architecture

Same `src/` conventions as the [React guide](/our-development-guides/react-guidelines#2-general-project-organization-and-architecture): `components/`, `hooks/`, `lib/`, `config.ts`. Folders are grouped **by role**, not by feature: every top-level folder holds one kind of file, and inside each folder things are grouped by domain (`posts`, `auth`, `cards`). Routing lives in `app/` following Expo Router's file conventions.

| Folder        | Holds                                                                                     | Never holds                                        |
| ------------- | ----------------------------------------------------------------------------------------- | -------------------------------------------------- |
| `app/`        | Expo Router routes only: screens and `_layout.tsx`                                        | Business logic, reusable components, data fetching |
| `components/` | All React components: `ui/` primitives, `shared/` app-level, one folder per route feature | Routes, pure utilities                             |
| `hooks/`      | Reusable hooks; `hooks/api/` for TanStack Query hooks                                     | One-off logic that belongs in a component          |
| `stores/`     | zustand stores (session, wizards)                                                         | Server state                                       |
| `contexts/`   | React Context providers for rarely-changing app state                                     | Server state, anything zustand should hold         |
| `lib/`        | Framework-agnostic code: API client, query client, i18n, icons                            | JSX                                                |
| `types/`      | Shared TypeScript types, grouped by domain                                                | Runtime code                                       |
| `constants/`  | Static values grouped by domain                                                           | Logic, env reads                                   |
| `config.ts`   | The only reader of `process.env`                                                          | Anything else                                      |

Older projects were bootstrapped without `src/`: the same folders sit at the repository root and `@/*` maps to the root. Expo supports both layouts. Don't migrate an existing project just for this, but never mix the two in one repo.

### 2.1. Expo project structure

```
app.json                  # Expo config: name, scheme, icons, plugins, experiments
eas.json                  # EAS build profiles and per-environment env (see 3.11)
plugins/                  # project-specific config plugins (see 3.8)
eslint.config.js
babel.config.js           # babel-preset-expo + nativewind/babel + module-resolver
metro.config.js           # withNativewind(getDefaultConfig())
openapi-ts.config.ts      # Hey API config (see 3.5)
lefthook.yml              # git hooks
assets/                   # images, fonts, icons
test/                     # Jest setup files (i18n, css stub)
src/
|- app/                   # Expo Router: routes only, thin wrappers (see 1.6)
|  |- _layout.tsx         # root layout: providers, splash screen, startup gating
|  |- index.tsx           # redirects to (auth) or (tabs) depending on the session
|  |- (auth)/             # public area: no tab bar
|  |  |- _layout.tsx      # Stack navigator
|  |  |- login.tsx        # /login
|  |  |- register.tsx     # /register
|  |- (tabs)/             # signed-in area, wrapped in AuthGuard
|     |- _layout.tsx      # Tabs navigator
|     |- home.tsx         # /home
|     |- posts/
|     |  |- _layout.tsx   # Stack navigator for this tab
|     |  |- index.tsx     # /posts
|     |  |- new.tsx       # /posts/new
|     |  |- [id].tsx      # /posts/:id
|     |- profile.tsx      # /profile
|- components/
|  |- ui/                 # design-system primitives: gluestack-ui CLI output (ours to edit, see 3.3)
|  |  |- index.ts         # barrel: import { Button, Text } from "@/components/ui"
|  |  |- button/
|  |  |- text/
|  |  |- form/            # react-hook-form kit (see 3.6)
|  |- shared/             # app-level components used by more than one feature
|  |  |- providers/
|  |  |  |- providers.tsx # provider stack used by the root layout (see 3.1)
|  |  |- error-boundary/
|  |  |  |- error-boundary.tsx
|  |  |- auth-guard/
|  |  |  |- auth-guard.tsx # redirects to (auth) when there is no session (see 3.10)
|  |  |- app-tabs/
|  |     |- app-tabs.tsx
|  |     |- app-tabs.web.tsx # platform-specific variant
|  |- auth/               # one folder per route feature, mirrors app/
|  |  |- login-form/
|  |  |  |- login-form.tsx
|  |  |- register-form/
|  |     |- register-form.tsx
|  |- posts/
|     |- post-list/       # one folder per component, always
|     |  |- post-list.tsx # screen component rendered by app/(tabs)/posts/index.tsx
|     |  |- post-list.spec.tsx
|     |- post-detail/
|     |  |- post-detail.tsx
|     |- post-form/
|     |  |- post-form.tsx
|     |  |- post-form.schema.ts
|     |- post-card/
|        |- post-card.tsx
|        |- components/   # local sub-components, same rule
|           |- post-card-footer/
|              |- post-card-footer.tsx
|- hooks/
|  |- api/                # TanStack Query hooks, one file per query or mutation (see 3.4)
|  |  |- use-posts.ts     # exports postKeys and usePosts
|  |  |- use-post.ts
|  |  |- use-create-post.ts
|  |  |- use-me.ts
|  |- use-color-scheme.ts
|  |- use-zod-form.ts
|  |- use-permissions.ts  # role booleans derived from useMe (see 3.10)
|- stores/
|  |- auth.store.ts       # zustand session store (see 3.10)
|- contexts/
|  |- theme-context.tsx
|- lib/
|  |- api/                # REST client (see 3.5)
|  |  |- client.ts        # base URL, auth headers, Accept-Language
|  |  |- generated/       # Hey API output, do not edit
|  |  |- posts.ts         # thin wrapper over the generated SDK
|  |- query/
|  |  |- client.ts        # QueryClient, onlineManager and focusManager wiring
|  |  |- invalidation.ts  # cross-domain invalidation groups (see 3.4)
|  |- i18n/
|  |  |- index.ts         # i18next instance and initI18n()
|  |  |- i18next.d.ts     # typed keys from en.json
|  |  |- locales/
|  |  |  |- en.json       # source of truth
|  |  |  |- es.json
|  |  |- native/          # expo.locales: app name and permission strings (see 3.7)
|  |     |- en.json
|  |     |- es.json
|  |- icons.ts            # lucide icons registered with cssInterop (see 3.3)
|- types/
|  |- posts/
|  |  |- post.types.ts    # domain types not covered by the generated client
|  |- auth/
|     |- auth.types.ts
|- constants/
|  |- theme.ts            # static token tables (theme colors for native props)
|  |- posts/
|     |- post-status.ts
|- config.ts              # the only reader of process.env.EXPO_PUBLIC_* (see 3.8)
|- global.css             # Tailwind v4 theme tokens (light and dark)
```

### 2.2. Components, hooks and domain folders

**Route feature to component folder, 1:1.** Every route feature under `app/` has a matching folder under `components/`: `app/(tabs)/posts/**` renders from `components/posts/**`, `app/(auth)/**` from `components/auth/**`. Adding a screen means adding or extending its component folder. The screen component (`post-list.tsx`) is the thing the route file renders; it composes smaller components from the same folder and hooks from `hooks/`.

**Three kinds of components.**

* `components/ui/`: design-system primitives with no domain knowledge (button, input, sheet, the form kit). gluestack-ui CLI output lives here. Re-exported from `components/ui/index.ts`, imported from the barrel
* `components/shared/`: app-level components used by more than one feature but too specific for `ui/` (`Providers`, `AuthGuard`, `EmptyState`, `AppTabs`)
* `components/<feature>/`: everything else, owned by one route feature

**One folder per component, always.** Every component lives in its own kebab-case folder containing a file of the same name (`post-card/post-card.tsx`). The folder is the unit of isolation: its spec, its zod schema, its styles helper and its local sub-components sit next to the component without cluttering the feature folder. Never drop loose `.tsx` files directly into `components/<feature>/`, `shared/` or `ui/`.

**Local sub-components stay local.** A component used only by one parent goes in a `components/` subfolder inside that parent's folder, following the same one-folder-per-component rule, not in the feature root or `shared/`:

```
components/posts/post-card/post-card.tsx
components/posts/post-card/post-card.spec.tsx
components/posts/post-card/components/post-card-footer/post-card-footer.tsx
components/posts/post-card/components/post-card-badge/post-card-badge.tsx
```

Promote to the feature root when a second component in the feature needs it, to `shared/` when a second feature needs it, and to `ui/` only when it has no domain knowledge left. This is the nearest common ancestor rule from the [React guide](/our-development-guides/react-guidelines#351-nextjs-app-router).

**Mirrored grouping.** `hooks/api/`, `types/`, `constants/` and `lib/api/` are grouped by the same domains as the route features (`posts`, `auth`, `cards`). A new type, constant or endpoint hook goes next to its domain siblings, so the domain is easy to grep across folders even though its files live in different roles.

### 2.3. Path aliases

Import from `src/` with the `@/` alias (`@/components/...`, `@/hooks/api/...`) and from `assets/` with `@/assets/...`. Configure it in three places and keep them in sync: `tsconfig.json` (`paths`), `babel.config.js` (`module-resolver`) and the Jest `moduleNameMapper`.

### 2.4. Tooling

* Package and script manager: [bun](https://bun.sh/) (`bun install`, `bun add`, `bun run <script>`, `bunx <cli>`), pinned with [mise](https://mise.jdx.dev/) (`mise.toml`) so everyone and CI run the same version
* Git hooks: [lefthook](https://github.com/evilmartians/lefthook), installed on `bun install` through the `prepare` script. Pre-commit lints staged files with `--fix` and `--max-warnings 0`; pre-push runs typecheck, lint and tests with coverage
* CI runs the same three commands: `lint -- --max-warnings 0`, `typecheck` and `test:cov`
* Scripts to expect in every project (run with `bun run <script>`): `start`, `android`, `ios`, `web`, `lint`, `lint:fix`, `typecheck`, `test`, `test:cov`, `api:generate`
* Projects with more than one service (the app plus its backend, a database, a mock server) run them together with [mprocs](https://github.com/pvolok/mprocs): one committed `mprocs.yaml` at the repository (or meta repository) root, one process per service, started with `bun run dev`. Every developer gets the same set of processes, one window, per-process logs and restarts. A single-service app doesn't need it: `bun run start` is enough

```yaml
# mprocs.yaml
procs:
  app:
    cwd: ./mobile
    shell: bun run start
  backend:
    cwd: ./backend
    shell: bun run dev
  db:
    shell: docker compose up postgres
```

## 3. Common patterns

### 3.1. Routing with Expo Router

[Expo Router](https://docs.expo.dev/router/introduction/) owns navigation. It is file-based like Next.js App Router, so the mental model from the React guide carries over:

* Files under `src/app/` are screens, folders are segments, `_layout.tsx` wraps its children with a navigator (`Stack`, `Tabs`, or `NativeTabs`) and persists across navigation
* `[id].tsx` for dynamic segments, `[...slug].tsx` for catch-all, `(group)` for route groups
* Navigation structure changes are file moves, not navigator config
* Enable **typed routes** (`experiments.typedRoutes: true` in `app.json`) and navigate with `Link` and `router` using typed hrefs. Never build route strings dynamically without the types catching it. Typed routes replace the `src/routes.ts` file of our web projects: the generated `.expo/types/router.d.ts` is the list of routes
* Read params with `useLocalSearchParams<{ id: string }>()` in the route file and pass them down as props
* Screen titles are set in the segment `_layout.tsx` with `Stack.Screen` options, translated with `useTranslation`. Never hardcode a title string, not even in the root layout
* Split public and authenticated areas into route groups: `(auth)/` for login, registration and onboarding, `(tabs)/` for the signed-in app. The `(tabs)/_layout.tsx` wraps its `Tabs` in an `AuthGuard` that renders `<Redirect>` when there is no session (see [3.10](#310-authentication-and-secure-storage)). `app/index.tsx` only decides where to redirect
* A tab that must exist as a route but not in the bar (a notifications list reached only from a bell icon) gets `href: null` in its `Tabs.Screen` options
* A screen that must react to becoming visible again (refresh a QR code, restart a timer) uses `useFocusEffect` from Expo Router, not `useEffect`. Screens in a stack stay mounted when covered, so `useEffect` does not fire on return
* Deep and universal links: declare the `scheme`, `ios.associatedDomains` and `android.intentFilters` in `app.json`. A link opened while logged out must survive the login: store the target (a `pendingPostId` in a small zustand store, for example) and redirect to it after authentication instead of dropping the user on the home screen

```tsx
// src/app/(tabs)/posts/_layout.tsx
import { Stack } from "expo-router";
import { useTranslation } from "react-i18next";

export default function PostsLayout() {
  const { t } = useTranslation();

  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: t("posts.title") }} />
      <Stack.Screen name="new" options={{ title: t("posts.new_title") }} />
      <Stack.Screen name="[id]" options={{ title: t("posts.detail_title") }} />
    </Stack>
  );
}
```

```tsx
// Linking from a feature component
<Link href={`/posts/${post.id}`} asChild>
  <Button size="sm" variant="outline">
    <ButtonText>{t("posts.view")}</ButtonText>
  </Button>
</Link>
```

The root `_layout.tsx` is where providers go, where `initI18n()` runs and where the splash screen is held until the app is ready. Rules for it:

* Keep the provider stack in one `Providers` component (`src/components/shared/providers/providers.tsx`): `QueryClientProvider`, `GluestackUIProvider`, `SafeAreaProvider`, `KeyboardProvider` from [react-native-keyboard-controller](https://kirillzyusko.github.io/react-native-keyboard-controller/), `ThemeProvider`. The layout file stays readable and tests can reuse the same stack
* Wrap everything in an `ErrorBoundary` that renders a translated fallback with a retry action. An uncaught render error must never leave the user on a blank screen
* Call `SplashScreen.preventAutoHideAsync()` at module scope, load custom fonts with `useFonts` (expo-font) and restore the session (see [3.10](#310-authentication-and-secure-storage)), then hide the splash screen once both are ready. Render nothing before that: a flash of the wrong screen is worse than a slightly longer splash
* Initialize third-party SDKs from one place at startup, not from screens (see [3.12](#312-third-party-sdks))
* The root layout gates the app in a fixed order before rendering the main navigator: backend reachable (a health request with a short timeout, otherwise a `network-error` screen with retry), onboarding completed (a flag in AsyncStorage, otherwise `(onboarding)`), session present (otherwise `(auth)`), then `(tabs)`. Put that order in one hook (`useInitializeApp()`), not spread over screens

### 3.2. Components and platform APIs

* Images: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/) (`Image`), never React Native core `Image`. Wrap it once with `styled(Image, { className: "style" })` from NativeWind and reuse that wrapper
* Lists: long or unbounded lists use `FlatList` (or [FlashList](https://shopify.github.io/flash-list/)), never `.map()` inside a `ScrollView`
* Animations: [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/), not the core `Animated` API. Gestures: [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/)
* Safe areas: [react-native-safe-area-context](https://github.com/AppAndFlow/react-native-safe-area-context) hooks and components, never hardcoded padding
* Platform differences: when a component diverges entirely per platform, use platform extensions (`component.web.tsx`, `component.ios.tsx`, `component.android.tsx`) next to the default file. Keep small inline differences in `Platform.select` or `Platform.OS` checks
* External links: open them with `expo-web-browser` on native and let `Link` behave as an anchor on web
* Screens that show secrets or payment material (QR codes, card numbers, one-time codes) call `usePreventScreenCapture()` from [expo-screen-capture](https://docs.expo.dev/versions/latest/sdk/screen-capture/) so they are blank in screenshots, recordings and the app switcher
* Haptics on meaningful actions (payment confirmed, code scanned) with [expo-haptics](https://docs.expo.dev/versions/latest/sdk/haptics/), never on every tap
* Follow the Rules of React strictly (no side effects during render, no mutating props or state): the React Compiler depends on them

### 3.3. Styling and theming

* Component library: [gluestack-ui](https://gluestack.io/) v5, built on NativeWind v5 and Tailwind v4. Do not install the legacy `@gluestack-ui/themed` packages
* Add components with the CLI (`bunx gluestack-ui add <component>`), never by hand-copying. Components are copied into `src/components/ui/` shadcn-style: they are ours to edit, so customize them in place instead of wrapping them in pass-through components. This mirrors the shadcn/ui setup of our web projects
* Recurring visual variations become variants on the copied component (its `tva` variant definitions), not ad-hoc `className` overrides at call sites
* Prefer an existing gluestack component or a variant of one over a one-off custom component
* Dark mode and theming go through the CSS variables in `global.css` (`:root`, `@media (prefers-color-scheme: dark)`, `:root.dark` and `:root.light` for the web class toggle), not through conditional `className` logic in components. NativeWind maps the media query to the device appearance on native
* The few native color props that cannot read CSS variables (`SymbolView`, native tabs, `tabBarActiveTintColor`) read a small `Colors` table in `src/constants/theme.ts` through a `useTheme()` hook. Keep that table minimal and never paste a hex value into a navigator option or a component
* Icons: [lucide-react-native](https://lucide.dev/guide/packages/lucide-react-native). Register every icon once with `cssInterop` in `src/lib/icons.ts` (so `className="text-muted-foreground"` colours it) and import icons from that file, never from the package directly
* Custom fonts: load them with `useFonts` in the root layout (Google fonts through `@expo-google-fonts/*`, brand fonts from `assets/fonts/`), register the families as theme tokens and use them through utilities (`font-heading`), never through `fontFamily` in a style
* Projects still on NativeWind v4 keep their tokens in `tailwind.config.ts` and use a `cn()` helper (clsx plus tailwind-merge) for conditional classes. That is fine for them: don't move tokens to `global.css` piecemeal. The upgrade to NativeWind v5 and gluestack-ui v5 is one dedicated change, not a side effect of a feature

### 3.4. Server state with TanStack Query

Same rules as in the [React guide](/our-development-guides/react-guidelines#312-global-state-management): TanStack Query v5 is the only manager for server state, and its cache is the global store for that data. Mobile additions:

* One `QueryClient` created in `src/lib/query.ts` and provided at the root layout. No per-feature clients
* Wire `onlineManager` to [@react-native-community/netinfo](https://github.com/react-native-netinfo/react-native-netinfo) and `focusManager` to `AppState`, so refetch-on-reconnect and refetch-on-focus work on native
* Every domain defines a query key factory (`postKeys`), exported from the hook file that owns the root key. Raw inline keys at call sites are forbidden, and invalidation always goes through the factory
* Components never call `useQuery` or `useMutation` directly. Every query and mutation is a named hook in its own file under `src/hooks/api/` (`use-posts.ts` exports `usePosts()`, `use-create-post.ts` exports `useCreatePost()`) that encapsulates key, query function and options. A mutation that must invalidate another domain imports that domain's key factory, so cross-domain cache wiring is explicit and greppable
* Mutations invalidate or update the cache in `onSuccess` or `onSettled` using the key factory. Use optimistic updates where waiting for the network feels sluggish, and always implement the rollback in `onError`
* Infinite queries for long lists set `maxPages` to bound memory
* Query keys include everything that changes the response: the params, but also the current user id, the selected tenant or company and `i18n.language` when the API returns localized content. Switching account, company or language must never show another scope's cached data
* Query hooks accept an `options` argument typed as `Pick<UseQueryOptions<...>, "enabled" | "placeholderData">` (add only what callers need) so a screen can tune a query without the hook exposing the whole TanStack surface
* Queries that depend on a param the screen may not have yet (an `id` from a deep link) set `enabled: !!id` instead of guarding in the component
* When one event changes several features at once (a new comment updates the post detail, the post list counters and the author profile), keep the list of affected key families in one helper (`src/lib/query/invalidation.ts`) and call it from the mutation. Don't spread `invalidateQueries` calls across hooks that then drift apart

```ts
// src/lib/query/client.ts
import NetInfo from "@react-native-community/netinfo";
import { QueryClient, focusManager, onlineManager } from "@tanstack/react-query";
import { AppState, Platform } from "react-native";

export const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 60 * 1000, retry: 2 } },
});

onlineManager.setEventListener((setOnline) =>
  NetInfo.addEventListener((state) => setOnline(!!state.isConnected)),
);

AppState.addEventListener("change", (status) => {
  if (Platform.OS !== "web") {
    focusManager.setFocused(status === "active");
  }
});
```

```ts
// src/hooks/api/use-posts.ts
export const postKeys = {
  all: ["posts"] as const,
  list: (status?: PostStatus) => [...postKeys.all, "list", status] as const,
  detail: (id: string) => [...postKeys.all, "detail", id] as const,
};

export function usePosts(status?: PostStatus) {
  return useQuery({ queryKey: postKeys.list(status), queryFn: () => postsApi.list(status) });
}
```

```ts
// src/hooks/api/use-create-post.ts
import { postKeys } from "./use-posts";

export function useCreatePost() {
  const queryClient = useQueryClient();
  return useMutation({
    mutationFn: (input: CreatePostInput) => postsApi.create(input),
    onSettled: () => queryClient.invalidateQueries({ queryKey: postKeys.all }),
  });
}
```

### 3.5. REST API client

As in the [React guide](/our-development-guides/react-guidelines#34-rest), generate the client from the backend's OpenAPI spec with [Hey API](https://heyapi.dev/) into `src/lib/api/generated/` (plugins: `@hey-api/client-fetch`, `@hey-api/typescript`, `@hey-api/sdk`, `@tanstack/react-query`). Never edit the output; regenerate with `bun run api:generate`. Mobile specifics:

* `src/lib/api/client.ts` is the only hand-written configuration: base URL, auth header and `Accept-Language` set to the device language so API error messages come back translated
* In development, default the base URL to the Expo dev server host read from `Constants.expoConfig?.hostUri`. That way simulators, USB and Wi-Fi devices reach a backend running on the developer's machine with no configuration. `EXPO_PUBLIC_API_URL` only overrides it (Android emulator without adb: `http://10.0.2.2:<port>`)
* Wrap the generated SDK per resource in `src/lib/api/<resource>.ts`: re-export the DTO types under domain names (`Post`, `CreatePostInput`), call the generated functions with `throwOnError: true` and unwrap `data`. Query hooks consume these wrappers, never the generated code directly
* Types flow from the API contract. Never retype responses locally

```ts
// src/lib/api/client.ts
import Constants from "expo-constants";

import { deviceLanguage } from "@/lib/i18n";

import { client } from "./generated/client.gen";

const devServerHost = Constants.expoConfig?.hostUri?.split(":")[0];
const apiToken = process.env.EXPO_PUBLIC_API_TOKEN;

client.setConfig({
  baseUrl: process.env.EXPO_PUBLIC_API_URL ?? `http://${devServerHost ?? "localhost"}:3002`,
  headers: {
    "Accept-Language": deviceLanguage(),
    ...(apiToken === undefined ? {} : { Authorization: `Bearer ${apiToken}` }),
  },
});

export { client };
```

**No OpenAPI spec, or backend not ready yet.** Fall back to the repository pattern, one repository per domain, all under `src/lib/api/`:

* `src/lib/api/posts/posts.repository.ts` declares the interface (`IPostsRepository`) in terms of the domain types from `src/types/posts/`
* `src/lib/api/posts/rest-posts.repository.ts` implements it against a small hand-written `apiClient` (`src/lib/api/client.ts`: base URL, auth headers, JSON, an `ApiError` class carrying `statusCode` and `fieldErrors`). Wire types (`PostWire`, snake\_case, exactly what the backend sends) and the `mapPost(wire): Post` functions live in this file and nowhere else. The rest of the app only sees camelCase entities
* `src/lib/api/posts/mock-posts.repository.ts` implements the same interface with fixtures and a `delay()`. It lets the UI ship before the backend exists and doubles as test data
* One typed registry (`src/lib/api/repository-registry.ts`) maps domain keys to instances: `getRepository("posts").getPost(id)`. Query hooks in `hooks/api/` call the registry, never a repository class, so switching from mock to REST is one line per domain
* Migrate to Hey API as soon as the backend publishes a spec. Wire types and mappers are exactly the code the generator would have written for you

### 3.6. Forms

[react-hook-form](https://react-hook-form.com/) plus [zod](https://zod.dev/) through `@hookform/resolvers`, as on the web. React Native has no `<form>` element and no native inputs that understand `onChange` events, so we keep a small form kit in `src/components/ui/form/`:

* `Form` renders the `FormProvider` plus a `Box`. Submission is a `Button` whose `onPress` calls `form.handleSubmit(onSubmit)()`
* `FormField`, `FormItem`, `FormLabel`, `FormControl` and `FormMessage` are the primitives (same names as the shadcn/ui form recipe). `FormControl` injects `isInvalid` into the gluestack input, so new wrappers around gluestack widgets should accept that prop
* Field components (`FormInput`, `FormTextarea`, `FormButtonGroup`) compose the primitives with the gluestack inputs and bind `value`, `onChangeText` and `onBlur`
* `useZodForm(schema, options)` in `src/hooks/` wraps `useForm` with the zod resolver, typed with the schema's input and output
* Validation messages are translated. Install [zod-i18n-map](https://github.com/aiji42/zod-i18n) as the global zod error map at i18n init so zod's built-in messages (required, too short, invalid email) come out in the user's language for free. Custom messages still go through `t`: schemas are factories taking `t` (`postSchema(t)`), built inside the component, with keys under `<feature>.form.errors.*`
* Single-choice fields with few options (statuses, roles) use `FormButtonGroup` with translated labels
* Type the schema against the API input type with `satisfies z.ZodType<CreatePostInput>` so the form and the contract cannot drift
* No English messages inside schemas. A shared `validation.ts` full of `"Password is required"` strings is a bug under the i18n rule even when the app ships in English only: use the `t` factory
* Server-side validation errors get the same treatment as client-side ones. Map the `fieldErrors` of an `ApiError` onto the form in one helper (`mapApiError(error, t, { fieldMap })`): known fields go to `form.setError(field, ...)`, `base` and unknown fields become a single global message shown above the submit button. Backend error symbols (`taken`, `invalid`) are translated through an `errors.<symbol>` key family, never shown raw
* Text inputs declare their intent: `keyboardType="email-address"`, `autoCapitalize="none"`, `autoCorrect={false}`, `secureTextEntry`, `textContentType`. It is the difference between a native-feeling form and a web form in a wrapper
* The submit button calls `Keyboard.dismiss()` before submitting, and forms live inside a keyboard-aware scroll view (react-native-keyboard-controller) so the focused field is never hidden behind the keyboard

### 3.7. Internationalization

[react-i18next](https://react.i18next.com/) with bundled JSON catalogs. Rules:

* The i18n instance lives in `src/lib/i18n/` and is created with `createInstance()`, not the implicit global. `initI18n()` runs once at the top of the root layout
* Language comes from the device ([expo-localization](https://docs.expo.dev/versions/latest/sdk/localization/) `getLocales()`), with `en` as fallback for unsupported languages. Never hardcode a language at call sites
* Catalogs are JSON files under `src/lib/i18n/locales/`, bundled with the app. No async backends
* `en.json` is the source of truth. Every other locale mirrors its key set exactly, enforced by a key-parity spec. Adding or removing a key touches every catalog in the same change
* Keys are snake\_case, nested by feature (`posts.form.title`, `posts.detail_title`, `common.retry`), shared copy under `common`. Key names describe meaning, never the English text
* Keys are typed: `i18next.d.ts` augments `CustomTypeOptions` from `en.json`, so `t()` and `<Trans>` reject unknown keys. Props that carry a key are typed `ParseKeys`, not `string`
* Read translations with `useTranslation()` inside components. Never call `i18n.t()` in render code and never resolve translations at module scope
* One key per complete sentence. Dynamic values use interpolation, countable copy uses plurals, inline elements use `<Trans>` with named `components`. Never build sentences by concatenating keys
* Enum-like values rendered to users map through a key family (`posts.status.<value>`). Never render the raw value
* Dates and numbers are formatted with the active language (`i18n.language`), never the bare device default
* Not translated: code, commands, file paths, scientific names, brand names, keyboard shortcuts, version strings
* Native strings are translated too: permission usage descriptions (`NSLocationWhenInUseUsageDescription`) and the app display name go through `expo.locales` in `app.json`, one JSON per language under `src/lib/i18n/native/`. Set `CFBundleAllowMixedLocalizations` so iOS picks them up
* Namespaces are optional. A big app can split `en.json` into one i18next namespace per feature plus `common` and `errors`, read with `useTranslation("posts")`. Pick nested keys or namespaces at the start and stay with it. Either way the key casing is snake\_case, and the typed keys and parity spec above still apply
* Tests run the real i18n with the English catalog (see [3.9](#39-testing)). Don't mock `react-i18next` to return keys: it hides missing keys and interpolation bugs, and it makes assertions read like `expect(getByText("auth:sign_in"))`

```ts
// src/lib/i18n/i18next.d.ts
import "i18next";

import type en from "./locales/en.json";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "translation";
    resources: { translation: typeof en };
  }
}
```

### 3.8. Configuration and environment

* Runtime config comes from `app.json` through [expo-constants](https://docs.expo.dev/versions/latest/sdk/constants/). Static config stays in `app.json`. When native config needs values from the environment (Google Maps keys per platform, the path to `google-services.json`), switch to `app.config.ts`, spread the static config and override only those fields
* Read `process.env.EXPO_PUBLIC_*` in exactly one file, `src/config.ts`, which exports a typed `config` object (booleans parsed, defaults applied). Components and hooks import from it; the string `process.env` does not appear anywhere else. This is the same `src/config.ts` as in the React guide
* Environment variables exposed to the app must use the `EXPO_PUBLIC_` prefix. Anything without it is unavailable in app code by design: never work around that. Anything with it ships in the bundle, so it is never a secret. Real secrets stay on the backend
* `EXPO_PUBLIC_*` values load when the dev server starts: restart Expo after editing `.env`. Commit a `.env.example` that documents every variable
* Enable `experiments.typedRoutes` and `experiments.reactCompiler` in `app.json`
* Web output: `web.output: "single"` (SPA) while NativeWind v5 does not support Expo's static SSR. Revisit when it does
* Patched dependencies live in `patches/` (bun `patchedDependencies`). Keep the patch when bumping the package and document it in the project's `CLAUDE.md`
* Project-specific config plugins live in `plugins/` and are referenced from `app.json` by relative path (`"./plugins/with-android-manifest-fix"`). Keep them tiny and commented: they run on every prebuild and are the only native code in the repo
* Native permissions are declared in `app.json` with a user-facing reason (`ios.infoPlist.NSPhotoLibraryUsageDescription`) and requested lazily, right before the feature that needs them. Never request all permissions at startup
* Apps that QA tests on real devices ship a hidden developer menu behind `EXPO_PUBLIC_ENABLE_DEV_TOGGLES` (true in `development` and `preview` builds, false in `production`): a long press on the tab bar opens a sheet with persisted toggles such as API request logging, mocked location or data, and skipping SMS or OTP validation. Every toggle reads from one `devToggles` state and defaults to off, and none of that code is reachable when the flag is false
* Wire [@dev-plugins/react-query](https://docs.expo.dev/debugging/devtools-plugins/) in the root layout so the TanStack Query cache is inspectable from the Expo dev tools
* Ship the third-party licenses: generate `assets/third-party-licenses.txt` with [generate-license-file](https://generate-license-file.js.org/) on every dependency change and show it from a screen in settings. Some clients and stores require it

### 3.9. Testing

* Runner: [jest-expo](https://docs.expo.dev/develop/unit-testing/) with [React Native Testing Library](https://callstack.github.io/react-native-testing-library/). Config lives in the `jest` block of `package.json`
* Coverage is collected from the trees where logic lives (`src/lib`, `src/hooks`, `src/stores`, `src/contexts`) with a 100% threshold. `src/app/**` (route wiring) and `src/components/**` (generated primitives, navigation and animation wiring) are excluded by scope, although presentational components in `components/<feature>/` and `components/shared/` still get colocated specs. New logic goes in a collected tree with its spec; never park logic under an excluded tree to dodge coverage
* Specs sit next to their source (`post-list.spec.tsx`)
* Query hooks are tested with `renderHook` and a fresh `QueryClient` (`retry: false`) per test, mocking the `src/lib/api/<resource>` wrapper
* Screen components are tested by mocking the `hooks/api` hooks they use and asserting on rendered English copy. A setup file (`test/setup-i18n.ts`, wired through `setupFilesAfterEnv`) mocks `expo-localization` and initializes `en`
* CSS imports are stubbed in `moduleNameMapper`
* Native modules that don't run under Jest (secure store, notifications, purchases, analytics SDKs) are mocked once in the setup file, not in every spec. Anything mocked there needs its own contract test elsewhere
* Shared test helpers (`createTestQueryClient()`, `renderHookWithProviders()`) live in `test/` and are excluded from test discovery with `testPathIgnorePatterns`
* Repository implementations (see [3.5](#35-rest-api-client)) are unit tested against a mocked `fetch`: wire fixture in, domain entity out. Hooks are tested with the repository mocked, so each layer is covered once
* End-to-end tests on device are optional and project-specific ([Maestro](https://maestro.mobile.dev/) is the tool to evaluate). The general rules from our [Testing guidelines](/our-development-guides/testing-guidelines) still apply: critical paths first, pure business logic as unit-tested functions

### 3.10. Authentication and secure storage

* Tokens live in [expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/) (Keychain on iOS, Keystore on Android). Never in AsyncStorage, never in a zustand `persist` middleware backed by AsyncStorage. AsyncStorage is for non-sensitive preferences (onboarding seen, last tab)
* The session (`user`, `tokens`, `isAuthenticated`) is a small zustand store (`src/stores/auth.store.ts`). It is the only client state that is read from almost everywhere, which is exactly the zustand case from the React guide
* One `bootstrapAuth()` runs at startup before the splash screen hides: read the stored session, validate its shape, hand the tokens to the API client and hydrate the store. A half-present session (tokens without user or the reverse) is discarded, not repaired
* The store is the source of truth and storage follows it: one subscription persists token and user changes to secure storage and pushes tokens to the API client. Screens call `login()` or `logout()` on the store and never touch storage directly
* The API client owns token lifecycle details: it attaches the headers, captures rotated tokens from responses and, on a `401` while holding tokens, clears them and notifies the store so the `AuthGuard` redirects. No screen handles `401`
* With access and refresh tokens, refresh before the request when the access token is expired (store the expiry with the tokens) instead of waiting for a `401`. Refreshes are single-flight: one in-flight refresh promise, and every caller that arrives meanwhile awaits it, so ten queries mounting together produce one refresh, not ten. Debounce the "session expired" handling too: when a token dies, every in-flight query fails at once and the user must see one toast and one redirect
* Role and permission checks live in one `usePermissions()` hook that derives booleans (`canManageCards`, `canInviteUsers`) from the current user and the selected company. Screens and components read those booleans; they never compare roles themselves
* Logout clears the store, secure storage, the TanStack Query cache (`queryClient.clear()`) and the identity of every third-party SDK, in one `clearLocalSession()` helper
* Sign-up flows with several steps (account, membership, connect a provider) persist the current step next to the session, so a killed app resumes where the user left off instead of restarting registration

### 3.11. Builds and releases with EAS

* Builds, store submission and over-the-air updates go through [EAS](https://docs.expo.dev/eas/). Nobody builds release binaries on a laptop
* Triggering is a different matter. EAS can start builds automatically only through its [GitHub integration](https://docs.expo.dev/build/building-from-github/). When the client hosts the code elsewhere (GitLab, Bitbucket, Azure DevOps, a self-hosted Forgejo), release builds are triggered locally with `bunx eas build --profile production --platform all` (and `bunx eas submit`) from a clean checkout of the release tag. The build still runs on EAS servers; only the trigger is local. Document the release steps in the project's `README.md`, and keep `eas.json` as the single source of truth so a local trigger and a GitHub trigger produce the same binary
* `eas.json` declares three profiles: `development` (`developmentClient: true`, `distribution: "internal"`), `preview` (internal distribution for testers) and `production` (`autoIncrement: true`). Add an `ios-simulator` profile that `extends` `preview` with `simulator: true` for reviewers without a device
* `EXPO_PUBLIC_*` values that differ per environment (API URL, third-party public keys) are set per profile in `eas.json` `env`, not in committed `.env` files. `.env` is for the developer's machine only
* `cli.appVersionSource` is `remote`: EAS owns the build number, `app.json` `version` is the marketing version and is bumped by hand in a release commit
* Everyday development uses a development build (`expo-dev-client`), not Expo Go, as soon as the app has a dependency with native code Expo Go doesn't ship. Expo Go stays useful for quick UI work, and every SDK wrapper must tolerate its native module being absent (see [3.12](#312-third-party-sdks))
* Pin the EAS image per platform (`ios.image: "sdk-57"`) so builds don't change under you when Expo publishes a new default
* Native builds for a physical test device don't need the cloud: `bunx eas build --local --profile preview --platform android` produces the same binary on a laptop, without spending build minutes. Release builds still go through the cloud
* Secret files that native builds need (`google-services.json`, `GoogleService-Info.plist`) are not committed. Each developer copies them from the password manager, `.gitignore` excludes them, and EAS gets them as file environment variables (`GOOGLE_SERVICES_JSON`) read from `app.config.ts`
* Add a `submit` profile to `eas.json` (App Store Connect app id, Play track) and expose the two release commands as scripts: `deploy:preview` and `deploy:production` (`eas build --profile production --auto-submit`). Releases are a documented script, not a remembered command
* A non-GitHub CI can still queue builds with a robot `EXPO_TOKEN` and `eas build --non-interactive --no-wait`. Use it only when the client asks for automated builds; the default for those hosts is the local trigger described above, documented in `README.md`

### 3.12. Third-party SDKs

Analytics, marketing, payments and crash reporting SDKs are the code most likely to break a build, crash Expo Go or leak into every feature. Rules:

* One wrapper module per SDK (`src/lib/analytics.ts`, `src/lib/purchases.ts`) exposing the three or four functions the app needs (`initialize`, `identifyUser`, `reset`). Nothing else imports the SDK package
* Configure them in the `Providers` module at startup, guarded by a `configured` flag, and no-op with a `__DEV__` warning when the public key for the platform is missing
* Gate on the native module being linked (`NativeModules.X != null`) and lazy-`require` the SDK inside the wrapper when its package reads native constants at import time. In Expo Go the wrapper becomes inert instead of crashing the app at boot
* Identify the user after login, registration, password reset and session restore, from the auth flow, in one place. Reset the identity on logout
* Anything that touches purchases goes behind a repository interface (`IBillingRepository`), so the paywall UI does not know it is talking to RevenueCat and tests never load the SDK
* SDK calls are fire-and-forget from the user's point of view: never gate login or navigation on an analytics or marketing call succeeding

## 4. Libraries

### 4.1. Recommended libraries

* Framework: [Expo](https://expo.dev/) (managed workflow, New Architecture)
* Routing: [Expo Router](https://docs.expo.dev/router/introduction/) with typed routes
* Components: [gluestack-ui](https://gluestack.io/) v5 (copied into `components/ui/`)
* Styling: [NativeWind](https://www.nativewind.dev/) v5 on Tailwind v4
* Images: [expo-image](https://docs.expo.dev/versions/latest/sdk/image/)
* Animations and gestures: [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/), [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/)
* Safe areas: [react-native-safe-area-context](https://github.com/AppAndFlow/react-native-safe-area-context)
* Server state: [TanStack Query](https://tanstack.com/query) v5, with [@react-native-community/netinfo](https://github.com/react-native-netinfo/react-native-netinfo) for online status
* Typed REST client: [Hey API](https://heyapi.dev/)
* Forms: [react-hook-form](https://react-hook-form.com/) with [zod](https://zod.dev/) and `@hookform/resolvers`
* Internationalization: [react-i18next](https://react.i18next.com/) and [expo-localization](https://docs.expo.dev/versions/latest/sdk/localization/)
* Client state: [zustand](https://github.com/pmndrs/zustand) when Context is not enough (same criteria as the React guide)
* Secure storage: [expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/) for tokens; [@react-native-async-storage/async-storage](https://github.com/react-native-async-storage/async-storage) for non-sensitive preferences only
* Icons: [lucide-react-native](https://lucide.dev/guide/packages/lucide-react-native)
* Keyboard: [react-native-keyboard-controller](https://kirillzyusko.github.io/react-native-keyboard-controller/)
* Builds and releases: [EAS](https://expo.dev/eas) Build, Submit and Update, with [expo-dev-client](https://docs.expo.dev/versions/latest/sdk/dev-client/) for development builds
* Testing: [jest-expo](https://docs.expo.dev/develop/unit-testing/), [React Native Testing Library](https://callstack.github.io/react-native-testing-library/)
* Lint and format: `eslint-config-expo`, [ESLint Stylistic](https://eslint.style/), `@tanstack/eslint-plugin-query`, `eslint-plugin-react-you-might-not-need-an-effect`, `eslint-plugin-simple-import-sort`
* Tooling: [bun](https://bun.sh/), [mise](https://mise.jdx.dev/), [lefthook](https://github.com/evilmartians/lefthook), [mprocs](https://github.com/pvolok/mprocs) for multi-service dev environments

### 4.2. Other libraries we have used

* Push notifications and marketing: [expo-notifications](https://docs.expo.dev/versions/latest/sdk/notifications/), [Klaviyo](https://github.com/klaviyo/klaviyo-react-native-sdk) (through its Expo config plugin)
* In-app purchases and subscriptions: [RevenueCat](https://www.revenuecat.com/docs/getting-started/installation/reactnative) (`react-native-purchases`, `react-native-purchases-ui`)
* OAuth flows with third-party providers: [expo-auth-session](https://docs.expo.dev/versions/latest/sdk/auth-session/) and [expo-web-browser](https://docs.expo.dev/versions/latest/sdk/webbrowser/)
* SVG assets as components: [react-native-svg](https://github.com/software-mansion/react-native-svg) with `react-native-svg-transformer`
* Maps: [react-native-maps](https://github.com/react-native-maps/react-native-maps) with [react-native-map-clustering](https://github.com/venits/react-native-map-clustering), [expo-location](https://docs.expo.dev/versions/latest/sdk/location/)
* Bottom sheets: [@gorhom/bottom-sheet](https://gorhom.dev/react-native-bottom-sheet/)
* PDF viewing and downloads: [react-native-pdf](https://github.com/wonday/react-native-pdf) and [react-native-blob-util](https://github.com/RonRadtke/react-native-blob-util), both through `@config-plugins/*`

### 4.3. Libraries worth taking a look into

* [FlashList](https://shopify.github.io/flash-list/) for long lists
* [Maestro](https://maestro.mobile.dev/) for end-to-end tests on device
* [expo-sqlite](https://docs.expo.dev/versions/latest/sdk/sqlite/) with [Drizzle](https://orm.drizzle.team/) for local persistence

## 5. Learning resources

* Expo docs, always for the SDK you use: <https://docs.expo.dev/>
* Expo Router: <https://docs.expo.dev/router/introduction/>
* React Native docs: <https://reactnative.dev/docs/getting-started>
* NativeWind: <https://www.nativewind.dev/>
* gluestack-ui: <https://gluestack.io/ui/docs>
* TanStack Query React Native notes: <https://tanstack.com/query/latest/docs/framework/react/react-native>


# TypeScript guidelines

## Start from the beginning

The best way to approach typing in an application is always from the foundations, defining the types of your data at the first moment it appears in your code.

* In a backend application that queries a database, start by typing your database models.
* In a frontend application that queries an API, start by typing the API responses.

You can save a lot of work by adding libraries to your stack that generate types automatically (TypeScript ORMs, API clients, Swagger/OpenAPI codegen, GraphQL codegen...). We recommend [Hey API](https://heyapi.dev/) (`@hey-api/openapi-ts`) to generate a fully typed client from an OpenAPI specification, and [Drizzle](https://orm.drizzle.team/) as the TypeScript ORM, defining your database schema in TypeScript and inferring the types of your models from it.

## Don't repeat yourself

Never repeat types, use and abuse [generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) and [utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) to derive them.

Some examples:

```ts
// This auxiliary type defines the return type of our REST client that responds with a type to be defined for each request
export type APIRequest<T> = Promise<{ data: T; statusCode: number }>;

// Each different user role
export enum UserRole {
  ADMIN = "admin",
  USER = "user",
}

// User interface as returned from the backend
export type User = {
  email: string;
  name: string;
  phone: number;
  role: UserRole;
  productIds?: number[];
};

// So, our user request response will be
export type UserRequestResponse = APIRequest<User>;

// Suppose the user creation form UI doesn't allow defining every User field, so we pick only what we need using Pick
// Also, for demonstration purposes, imagine that the form is filled in different steps and is not fully completed from the beginning,
// so we can mark every field as optional (?) using Partial
export type CreateUserFormData = Partial<Pick<User, "email" | "name" | "phone">>;

// But for the method that will send the request, those fields are required, so we can remove every optional field (?) with Required
export type CreateUserPayload = Required<CreateUserFormData>;

// When obtaining the new user response, it could be useful to populate the user products with the full product objects obtained
// from other requests, so the user returned from the method once populated could be:
export type PopulatedUser = Omit<User, "productIds"> & { products: Product[] };

// In a React application, suppose a card component that exposes User information, its props could be
export type UserCardProps = {
  user: PopulatedUser
}
export function UserCard({ user }: UserCardProps) { ... }
```

## Prefer types over interfaces

Use `type` instead of `interface` for defining object shapes and other type aliases. `type` is more versatile and consistent across different use cases.

**Advantages of `type`:**

* Can represent unions, intersections, tuples, mapped types, and conditional types — `interface` cannot
* More consistent: one syntax for all type definitions
* Cannot be declaration-merged (prevents accidental augmentation from other files)
* Works more naturally with utility types (`Pick`, `Omit`, `Partial`, etc.)

What `type` can do that `interface` cannot:

```ts
// ✅ type can represent unions
type Status = "pending" | "active" | "disabled";

// ✅ type can represent intersections concisely
type AdminUser = User & { permissions: string[] };

// ✅ type can represent tuples
type Coordinate = [number, number];

// ✅ type can use mapped and conditional types
type Readonly<T> = { readonly [K in keyof T]: T[K] };
type NonNullableFields<T> = { [K in keyof T]: NonNullable<T[K]> };

// ❌ None of the above are possible with interface
```

Declaration merging pitfall:

```ts
// ⚠️ interface allows declaration merging, which can cause unexpected behavior
// In file user.ts
interface User {
  name: string;
  email: string;
}

// In another file or even the same file
interface User {
  role: string; // This silently merges into the original User interface
}

// Now User has name, email, AND role — which may be unintended

// ✅ type does not allow declaration merging — redeclaring causes a compile error
type User = {
  name: string;
  email: string;
};

type User = { // ❌ Error: Duplicate identifier 'User'
  role: string;
};
```

> **Exception:** `interface` is acceptable when you intentionally need declaration merging (e.g., augmenting third-party library types or extending `Window`).

## Use explicit return types for complex functions

Functions returning complex types that aren't easily inferred must have explicit return type annotations. Simple functions with obvious inference don't need them.

**Why explicit return types matter:**

* Acts as documentation of the function contract
* Catches implementation errors at the function boundary, not at distant call sites
* Prevents accidental return type changes from silently propagating
* Produces better, more localized error messages

**Problems when omitted:**

* A small implementation change can accidentally alter the inferred return type, breaking callers far away
* Error messages appear at call sites instead of at the function definition
* Harder to understand what a function returns without reading its full implementation

Accidental return type change without explicit annotation:

```ts
// ❌ Bad: No explicit return type
function getUserDisplayData(user: User) {
  return {
    fullName: `${user.firstName} ${user.lastName}`,
    email: user.email,
    role: user.role,
  };
}

// Later, someone refactors and accidentally changes the return shape:
function getUserDisplayData(user: User) {
  return {
    fullName: `${user.firstName} ${user.lastName}`,
    email: user.email,
    role: user.role,
    // Accidentally added — now the inferred return type changes silently
    internalId: user.id,
  };
}

// The error only surfaces far away at call sites:
// "Property 'internalId' does not exist on type..." — confusing and hard to trace
```

```ts
// ✅ Good: Explicit return type catches the mistake immediately at the function
type UserDisplayData = {
  fullName: string;
  email: string;
  role: UserRole;
};

function getUserDisplayData(user: User): UserDisplayData {
  return {
    fullName: `${user.firstName} ${user.lastName}`,
    email: user.email,
    role: user.role,
    internalId: user.id, // ❌ Error right here: 'internalId' does not exist in type 'UserDisplayData'
  };
}
```

When explicit return types aren't needed:

```ts
// Simple functions with obvious inference don't need explicit return types
const add = (a: number, b: number) => a + b;
const isActive = (user: User) => user.status === "active";
const toUpperCase = (value: string) => value.toUpperCase();
```

## Use named types, avoid anonymous types

Prefer named types over inline/anonymous type literals. Named types improve reusability, readability, and maintainability.

**Advantages:**

* Reusable across the codebase
* Better error messages — TypeScript shows the type name instead of the full expanded structure
* Self-documenting: a name communicates intent
* Easier refactoring — change the type definition in one place
* Better IDE experience (hover tooltips show meaningful names)

**Problems with anonymous types:**

* Cannot be reused, leading to duplication
* Error messages show the full object structure, making them hard to read
* No single source of truth — changes must be made in every occurrence

Anonymous types causing duplication and poor readability:

```ts
// ❌ Bad: Anonymous types repeated across the codebase
function filterUsers(
  users: User[],
  filters: { status: string; role: string; active: boolean },
): User[] {
  // ...
}

function buildFilterQuery(
  filters: { status: string; role: string; active: boolean },
): string {
  // ...
}

// If the filter shape changes, you have to update it everywhere
// Error messages will show the full object structure:
// "Argument of type '{ status: string; }' is not assignable to
//  parameter of type '{ status: string; role: string; active: boolean }'"
```

```ts
// ✅ Good: Named type used everywhere
type UserFilters = {
  status: string;
  role: string;
  active: boolean;
};

function filterUsers(users: User[], filters: UserFilters): User[] {
  // ...
}

function buildFilterQuery(filters: UserFilters): string {
  // ...
}

// Single source of truth — change in one place
// Error messages are clear:
// "Argument of type '{ status: string; }' is not assignable to
//  parameter of type 'UserFilters'"
```

Anonymous types in React components:

```tsx
// ❌ Bad: Anonymous prop types
export function UserCard({ name, email, role }: { name: string; email: string; role: string }) {
  // ...
}

// ✅ Good: Named, exported prop types
export type UserCardProps = {
  name: string;
  email: string;
  role: string;
};

export function UserCard({ name, email, role }: UserCardProps) {
  // ...
}
```

See also the component conventions in our [React guidelines](/our-development-guides/react-guidelines#18-do-name-prop-types).

### Export every type

In an ideal world, all libraries would export the types of the objects they provide access to. Unfortunately, this is not always the case, so export every type you create. If you find yourself working with types that you don't have direct access to, create your derivatives as soon as possible. Unwrapping an inaccessible type can be too complex and is often a verbose and difficult-to-read operation. If all types were exported, we could avoid things like:

```ts
// Imagine an external library that doesn't export its types
// We have to 'unwrap' the types we need
type OperationResult<T> = Awaited<ReturnType<typeof calculationLib["calculateNow"] extends (arg: infer A) => infer R ? (arg: T) => R : never>>;

type OperationData = { a: number; b: number };
const data: OperationData = { a: 2, b: 3 };

// Complex usage with extensive type annotations
const result: OperationResult<OperationData> = await calculationLib.calculateNow(
  data,
  { someOptions: { operation: 'a + b', someFlag: true } }
);

// Trying to extract option types
type SumOptions = Omit<Parameters<(typeof calculationLib)["calculateNow"]<OperationData>>[1]["someOptions"], 'operation'>;

// Helper function to simplify usage, but with complex types
const sum = (a: number, b: number, options: SumOptions): Promise<OperationResult<OperationData>> =>
  calculationLib.calculateNow(
    { a, b },
    { someOptions: { operation: 'a + b', ...options } }
  );

const sumResult = sum(3, 5, { someFlag: false });
```

Instead of the above, with exported types it would be simpler:

```ts
import { Operation, OperationOptions, OperationResult } from "calculation-lib";

const sum = (
  a: number,
  b: number,
  options: Omit<OperationOptions, "operation">,
): Promise<OperationResult<number>> =>
  calculationLib.calculateNow({ a, b }, { operation: "a + b", ...options });
```

### Avoid casting when possible

Excessive use of casting (`as Type`, `<Type>value`) often indicates problems in the design of types or the structure of the code. If you find yourself using many casts, you may be fighting against the type system rather than leveraging it.

Casts create blind spots in the type system, as you're telling the compiler to "trust you" rather than properly verifying types. This can lead to runtime errors that are difficult to detect.

```ts
// ❌ Bad: Excessive use of casting
function processData(data: any) {
  const user = data as User;
  const products = (data.items as any[]).map((item) => item as Product);
  return {
    user,
    products,
    total: data.total as string as unknown as number,
  };
}
```

#### Alternatives to casting

1. **Type Guards**: Functions that help TypeScript recognize types at runtime.

```ts
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function processValue(value: unknown) {
  if (isString(value)) {
    // Here TypeScript knows that value is a string
    return value.toUpperCase();
  }
  return String(value);
}
```

2. **Assertion Functions**: Functions that throw an error if the condition is not met.

```ts
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Value must be a string");
  }
}

function processValue(value: unknown) {
  assertIsString(value);
  // Here TypeScript knows that value is a string
  return value.toUpperCase();
}
```

3. **Validation Schemas**: Use libraries like Zod, io-ts, or Ajv to validate and type data simultaneously.

```ts
import { z } from "zod";

const UserSchema = z.object({
  email: z.string().email(),
  name: z.string(),
  age: z.number().int().positive(),
});

type User = z.infer<typeof UserSchema>;

function processUser(data: unknown) {
  // Validates and converts data to User
  const user = UserSchema.parse(data);

  // user is fully typed as User
  return `${user.name} (${user.email})`;
}
```

The goal should always be to create code that is type-safe by design, rather than forcing types with casting.

### Don't carry nullable values in function parameters

An important principle in type design is to avoid "carrying" nullable values (`null` or `undefined`) through the function chain. If a parameter can be nullable, it's better to handle it as early as possible in your code.

When you allow nullable values to propagate through multiple functions, each function needs to check if the value is nullable, which causes:

1. Code repetition (each function repeats the same checks)
2. Increased complexity (code full of conditional checks)
3. Higher probability of errors (if a check is forgotten)
4. Less readable and harder to maintain code

```ts
// ❌ Bad: Propagating nullable values through multiple functions
function getUserByEmail(email: string | null): User | null {
  if (!email) return null;
  return findUser(email);
}

function getUserName(email: string | null): string {
  const user = getUserByEmail(email);
  // Now we have to check again if user is null
  return user ? user.name : "Unknown User";
}

function greetUser(email: string | null): string {
  const name = getUserName(email);
  return `Hello, ${name}!`;
}

// This works, but each function must handle the null case
const greeting = greetUser(email);
```

Instead, it's better to validate nullable values as early as possible and work only with non-nullable values:

```ts
// ✅ Good: Early handling of nullable values
function getUserByEmail(email: string): User | null {
  return findUser(email);
}

function getUserName(user: User): string {
  return user.name;
}

function greetUser(name: string): string {
  return `Hello, ${name}!`;
}

// Handling the nullable case in one place
function processGreeting(email: string | null): string {
  if (!email) return "Hello, visitor!";

  const user = getUserByEmail(email);
  if (!user) return "Hello, user not found!";

  const name = getUserName(user);
  return greetUser(name);
}

const greeting = processGreeting(email);
```

#### Benefits of early nullable handling

1. **Simpler functions**: Each function has a clear purpose and doesn't worry about nullable values.
2. **Better type inference**: TypeScript can infer more precise types, reducing the need for type annotations.
3. **Safer code**: Less likelihood of `TypeError: Cannot read property 'x' of null` errors.
4. **Better testability**: Functions with non-nullable inputs are easier to test.

#### Techniques for handling nullable values

1. **Early validation**:

```ts
function processData(data: string | null | undefined): void {
  if (data == null) {
    // Handle the nullable case
    return;
  }

  // From here on data is a string
  const upperCaseData = data.toUpperCase();
  // ...
}
```

2. **Default values**:

```ts
function processConfig(config: Config = defaultConfig): void {
  // We always work with a non-nullable value
  const timeout = config.timeout ?? 5000;
  // ...
}
```

3. **Pattern matching / discriminated unions**:

```ts
type Result<T> = { ok: true; value: T } | { ok: false; error: string };

function processResult<T>(result: Result<T>): void {
  if (!result.ok) {
    // Error handling
    console.error(result.error);
    return;
  }

  // TypeScript knows that result.value is present
  const value = result.value;
  // ...
}
```

This approach of handling nullable values early in the application flow and working with non-nullable types in most of the code leads to a more robust and easier to maintain system.

#### Nullables in React functional components

This principle is **especially important** in React functional components. React components often receive props that may be nullable, and it's easy to fall into patterns where these checks are repeated in multiple components or in multiple parts of the same component.

```tsx
// ❌ Bad: Propagating nullable props in nested components
export type UserCardProps = {
  user: User | null;
};

export function UserCard({ user }: UserCardProps) {
  // Repetitive check
  if (!user) return <div>No user</div>;

  return (
    <div className="user-card">
      <UserHeader user={user} />
      <UserDetails user={user} />
      <UserActions user={user} />
    </div>
  );
}

export function UserHeader({ user }: UserCardProps) {
  // We have to check again
  if (!user) return null;

  return <h2>{user.name}</h2>;
}

export function UserDetails({ user }: UserCardProps) {
  // And again
  if (!user) return null;

  return (
    <div className="details">
      <p>Email: {user.email}</p>
      {/* ... */}
    </div>
  );
}
```

Better approach:

1. **Validation in the main component**:

```tsx
export type UserProfileProps = { userId: string | null };

export function UserProfile({ userId }: UserProfileProps) {
  // Handle the nullable once
  if (!userId) return <div>Please select a user</div>;

  return <UserProfileContent userId={userId} />;
}

// This component always receives a non-nullable userId
export type UserProfileContentProps = { userId: string };

export function UserProfileContent({ userId }: UserProfileContentProps) {
  // We don't need to check if userId is null
  const { data: user, loading, error } = useUser(userId);

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  if (!user) return <NotFoundMessage />;

  // From here we know that user is present
  return (
    <div>
      <UserHeader name={user.name} avatar={user.avatar} />
      <UserDetails email={user.email} phone={user.phone} />
      {/* ... */}
    </div>
  );
}

// Components receive only the specific data they need
export type UserHeaderProps = { name: string; avatar: string };

export function UserHeader({ name, avatar }: UserHeaderProps) {
  return (
    <header>
      <img src={avatar} alt={name} />
      <h1>{name}</h1>
    </header>
  );
}
```

2. **Using nullish coalescing operators and default values in props**:

```tsx
export type UserAvatarProps = {
  user?: User;
  size?: "small" | "medium" | "large";
  fallbackImage?: string;
};

export function UserAvatar({
  user,
  size = "medium",
  fallbackImage = "/images/default-avatar.png",
}: UserAvatarProps) {
  // Using optional chaining with fallback
  const avatarUrl = user?.avatarUrl ?? fallbackImage;
  const userName = user?.name ?? "Unknown User";

  return (
    <img
      src={avatarUrl}
      alt={`Avatar of ${userName}`}
      className={`avatar-${size}`}
    />
  );
}
```


# Back-end guidelines

## Do's and Don'ts

Do's

* Always be looking at the applications logs while developing.
  * Look for N+1 queries
  * Look for warnings, etc.
* Use REST routes always if possible (a resource does not need to be an entity backed by the DB, an "export" can be a resource for example).
* Always use i18n from the get-go even if building for a single language.

Don'ts

* We try to avoid writing code that has unexpected side effects (sending an e-mail, making an API call) as much as possible. In particular this affects the usage of callbacks. We minimize the use of model callbacks and only use them to do some calculation at the persistence layer or some simple operation.

## Common patterns

* Database: [PostgreSQL](https://www.postgresql.org/) unless there is a project requirement that specifies to use another database engine, or use an existing database.
* [Redis](https://redis.io/): In-memory data store commonly used for:
  * **Caching**: Store frequently accessed data (API responses, computed values, database query results) to reduce load on the database and improve response times.
  * **Background job queues**: Many job processing systems use Redis to manage queues. Configure separate queues for different job priorities (default, mailers, critical, low).
  * **Mutex locking**: Prevent race conditions in background jobs using distributed locks. Essential when multiple workers might process the same resource simultaneously.
  * **Temporary blocking/rate limiting**: Block access to resources temporarily (e.g., prevent duplicate form submissions, implement API rate limiting, or throttle requests per user/IP).
  * **Session storage**: Store user sessions in Redis for faster access and easier horizontal scaling across multiple application servers.
  * **Real-time features**: Pub/Sub for WebSocket connections or inter-process communication.
* Services:
  * MarsBased self-hosted tracking solution or [Sentry](https://sentry.io/) for error reporting
  * [Wasabi](https://wasabi.com/) or [AWS S3](https://aws.amazon.com/s3/) for uploads
  * [Cloudflare](https://www.cloudflare.com/) for CDN / reverse proxy. If the final users are from Spain we avoid Cloudflare as it can be blocked by Spanish ISPs.
  * [GitHub Actions](https://github.com/features/actions) for CI
  * [Mailgun](https://www.mailgun.com/) for email sending
  * Rely on the hosting service for monitoring, metrics and logs. Advanced monitoring needs would require a dedicated service like [Datadog](https://www.datadoghq.com/).
* Usage of commands / services to keep controllers thin and models responsible mainly for persistence.
  * Don't use commands for everything. Limit their usage when you cannot achieve the same functionality by using the ORM directly by the controller.
  * When you have 2 or more related commands, namespace them into the same module (Users, Purchases, etc) to prevent a huge commands folder difficult to manage. If you cannot find a command in a big project, you cannot reuse it.


# Ruby & Rails guidelines

We follow the `Rubocop` official [Rubocop Ruby coding style guide](https://github.com/rubocop/ruby-style-guide) as the primary source of best practices and conventions.

* 1. [Do's and Don'ts](#1-dos-and-donts)
  * 1.1. [Environment Variables](#11-use-dotenv-for-environment-variables)
  * 1.2. [Loading in batches](#12-loading-in-batches)
  * 1.3. [Avoid Active Record callbacks with side effects](#13-avoid-active-record-callbacks-with-side-effects)
  * 1.4. [Avoid raw SQL queries](#14-avoid-raw-sql-queries)
  * 1.5. [Size instead of count](#15-size-instead-of-count)
  * 1.6. [Avoid N+1 Queries with includes](#16-avoid-n1-queries-with-includes)
  * 1.7. [Avoid Default Scope](#17-avoid-default-scope)
  * 1.8. [Use find\_by for instead where().first](#18-use-findby-for-instead-wherefirst)
  * 1.9. [Check constraints](#19-check-database-constraints)
  * 1.10. [Filter sensitive parameters in logs](#110-filter-sensitive-parameters-in-logs)
* 2. [General project organization and architecture](#2-general-project-organization-and-architecture)
  * 2.1. [Project structure example](#21-project-structure-example)
* 3. [Common Patterns](#3-common-patterns)
  * 3.1. [Devise (Authentication)](#31-devise-authentication)
  * 3.2. [Testing](#32-testing)
    * 3.2.1 [Testing best practices](#321-testing-best-practices)
      * 3.2.1.1 [Use Let](#3211-use-let)
      * 3.2.1.2 [Use Factories](#3212-use-factories)
      * 3.2.1.3 [Describe Methods](#3213-describe-methods)
* 4. [Gems](#4-gems)

## 1. Do's and Don'ts

Add and follow the official [MarsBased Rubocop configuration](https://github.com/MarsBased/marstyle/blob/master/ruby/.rubocop.yml), where most of rules are already defined, highlighting these two:

* Use single quotes when possible.
* Max length of 90 characters

### 1.1. Use Dotenv for environment variables

We use the [Dotenv](https://github.com/bkeepers/dotenv) gem for managing the environment variables.

### 1.2. Loading in batches

Don't iterate unlimited / big queries directly. Use find in batches for loading big queries:

```ruby
# WRONG
Car.all.each do |car|
  car.start_engine!
end

# RIGHT
Car.find_each do |car|
  car.start_engine!
end
```

### 1.3. Avoid Active Record callbacks with side effects

Avoid using Active Record callbacks unless it's related to data persistence specially avoiding side effects like sending and e-email.

Consider using the [command pattern](https://github.com/MarsBased/handbook/blob/master/guides/patterns/rails/command.md) to send the e-mail from a controller action.

```ruby
# WRONG
after_save :notify_user

def notify_user
  UserMailer.notify(user).deliver
end
```

### 1.4. Avoid raw SQL queries

Avoid writing raw SQL queries unless strictly necessary.

When using Active Record we have the full power of it. For example if we have a custom serialization for a column, Active Record will automatically convert the value when writing queries.

```ruby
# WRONG
User.where('active = ?', params[:active])

# RIGHT
User.where(active: params[:active])
```

When writing more complex queries you may use Arel or write the where clause manually. However take into account that if you write it manually you won't have the full power of Active Record, like:

* You will not be able to use alias attributes.
* You will not be able to use custom types (serialization and deserialization).

```ruby
class User < ApplicationRecord
  alias_attribute :created_at, :dtCreationDate
end

# MANUAL
User.where('dtCreationDate < ?', DateTime.current) # Needs to use column name in the database

# AREL
User.where(User.arel_table[:created_at].lt(DateTime.current)) # Can use aliased name
```

### 1.5. Size instead of count

Use `size` instead of `count` unless you are doing a direct count on a table. Using `count` always triggers a query while using `size` is able to use the cached values of a previous query.

```ruby
# WRONG
Post.published.count

# RIGHT
Post.published.size

# RIGHT
Post.count # Counting directly on the model class
```

### 1.6. Avoid N+1 Queries with includes

When you have to access an association, avoid N+1 query problems, you can use `includes` to eager load the associated records:

```ruby
# WRONG
User.all.each do |user|
  user.posts.each do |post|
    p post.title
  end
end

# RIGHT
User.includes(:posts).each do |user|
  user.posts.each do |post|
    p post.title
  end
end
```

You can find some more examples in the [Active Record guide](/our-development-guides/activerecord-guide).

### 1.7. Avoid Default Scope

In order to avoid unexpected and hidden behaviour, avoid using default\_scope and use named scopes and explicit uses of those scopes:

```ruby
# WRONG
class User < ActiveRecord::Base
  default_scope { where(deleted: false) }
end

# RIGHT
class User < ActiveRecord::Base
  scope :active, -> { where(deleted: false) }
end
```

### 1.8. Use find\_by for instead where().first

When retrieving a single record from the database, don’t use `where(...).first`, use `find_by` instead. And similarly when selecting a single item from a collection use `find { ... }` instead of `select { ... }.first`.

```ruby
# WRONG
User.where(active: true).first

# RIGHT
User.find_by(active: true)
```

### 1.9. Check database constraints

Check that constraints are correct and that they match the validations. A typical example is adding a default without a not-null constraint.

### 1.10. Filter sensitive parameters in logs

When receiving parameters in a controller that contain sensitive information like a password or secret key, add the name of the parameter to the list of filtered parameters. Note that `:password` is already filtered by default.

```ruby
Rails.application.config.filter_parameters += [:api_key, :secret]
```

## 2. General project organization and architecture

Follow the standard generated directory structure at project initialization with `rails new project_name` as described in [Ruby On Rails Guide](https://guides.rubyonrails.org/getting_started.html).

Additionally:

* Services under the `/app/services` directory.
* Commands under the `/app/commands` directory.
* Presenters under the `/app/presenters` directory.
* Query objects under the `/app/queries` directory.
* Form objects under the `/app/form_objects` directory.

### 2.1. Project structure example

```
app/
 |- assets/
 |- channels/
 |- controllers/
 |- helpers/
 |- jobs/
 |- mailers/
 |- models/
 |- form_objects/
 |- queries/
 |- presenters/
 |- services/
 |- commands/
 |- views/
bin/
config/
 |-environments/
 |-initializers/
 |-locales/
db/
 |- migrate/
lib/
 |-assets/
 |-tasks/
log/
public/
spec/
 |- factories/
 |- helpers/
 |- mailers/
 |- models/
 |- requests/
 |- support/
 |- views/
tmp/
vendor/
```

## 3. Common Patterns

* [Presenter](https://github.com/MarsBased/handbook/guides/patterns/rails/presenter.md)
* [Command](https://github.com/MarsBased/handbook/guides/patterns/rails/command.md)
* [Form Composition](https://github.com/MarsBased/handbook/guides/patterns/rails/form-composition.md)
* [Query Object](https://github.com/MarsBased/handbook/guides/patterns/rails/query-object.md)

### 3.1. Devise (Authentication)

Skip all the default routes generated by devise on `routes.rb` and create custom controllers and views according to the requirements.

#### `routes.rb`

```ruby
devise_for :users, skip: :all
resource :sign_up, only: %i(new create), path_names: { new: '' }
resource :session, only: %i(new create destroy), path: 'login', path_names: { new: '' }
resource :confirmation, only: %i(new create show)
resource :password, only: %i(new create edit update)
```

#### Sessions Controller example: `app/controllers/sessions_controller.rb`

```ruby
class SessionsController < ApplicationController

  prepend_before_action :allow_params_authentication!, only: :create
  prepend_before_action :require_no_authentication, only: %i(new create)

  def new
    @user = User.new
    @user.clean_up_passwords
  end

  def create
    @user = authenticate_user!(recall: 'sessions#new')
    sign_in(@user)

    redirect_to sign_up_path, notice: t('.ok')
  end

  def destroy
    sign_out(:user)

    redirect_to new_session_path, notice: t('.ok')
  end

end
```

#### New session view example: `app/views/sessions/new.html.erb`

```erb
<div class="box">
  <hgroup class="login__header">
    <h1><%= t('.title') %></h1>
  </hgroup>

  <%= simple_form_for(@user,
                    url: session_path,
                    html: { class: 'login__form' }) do |form| %>
    <%= form.input :email %>
    <%= form.input :password %>
    <%= form.submit t('.submit'), class: 'btn-primary is-block' %>
    <p class="login__link">
      <%= link_to 'Forgot Password?', new_password_path, class: 'link--secondary'%>
    </p>
  <% end%>
</div>
```

### 3.2. Testing

We use the [Rspec](https://github.com/rspec/rspec) testing framework and usually we write these kind of tests:

* Unit tests for models, commands, jobs.
* System tests for integration.
* Request specs for APIs.
* Avoid controller tests: controllers functionality is already covered by integration specs.

### 3.2.1. Testing Best Practices

#### 3.2.1.1 Use let

When you have to assign a variable to test, instead of using a before each block, use let. It is memoized when used multiple times in one example, but not across examples.

```ruby
describe User do
  let(:user) { User.new(name: 'Rocky Balboa') }

  it 'has a name' do
    expect(user.name).to_not be_nil
  end
end
```

#### 3.2.1.2 Use factories

Use [factory\_bot](https://github.com/thoughtbot/factory_bot) to reduce the verbosity when working with models.

**`spec/factories/user.rb`**

```ruby
FactoryBot.define do
  name { 'Rocky Balboa '}
  age { 30 }
  active { true }
  role { :engineer }
end
```

**Using the factory**

```ruby
user = FactoryBot.create(:user, name: 'John Rambo') # The rest of attributes are already set
```

#### 3.2.1.3 Describe Methods

When testing a method, create a describe block with the name of the method and place the specs inside. Use "." as prefix for class methods and "#" as prefix for instance methods.

```ruby
describe ".authenticate" do
  it 'returns true when the user is active' { ... }
  it 'returns false when the user is deleted' { ... }
end

describe "#generate_export" do
  it 'returns an empty array when there are not users' { ... }
  it 'returns the list of active users' { ... }
end
```

## 4. Gems

* General
  * [Keynote](https://github.com/rf-/keynote) for presenters.
  * [Simple form](https://github.com/heartcombo/simple_form) for form generation.
  * [Dotenv + dotenv-rails](https://github.com/bkeepers/dotenv) for environment variables.
  * [Activeadmin](https://github.com/activeadmin/activeadmin) for admin panels.
  * [Devise](https://github.com/heartcombo/devise) for authentication.
  * [Sidekiq](https://github.com/sidekiq/sidekiq) for background jobs.
  * [Sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron) for scheduled jobs.
  * [Sidekiq-failures](https://github.com/mhfs/sidekiq-failures) for error tracking in background jobs.
  * [Shrine](https://github.com/shrinerb/shrine) for file uploads.
  * [Http (http-rb)](https://github.com/httprb/http) for http calls.
  * [Friendly\_id](https://github.com/norman/friendly_id) for slugged url generation.
  * [Kaminari](https://github.com/kaminari/kaminari) for pagination.
  * [Jbuilder](https://github.com/rails/jbuilder) for JSON API responses.
  * [Pundit](https://github.com/varvet/pundit) for authorization.
* Testing:
  * [Rspec](https://github.com/rspec/rspec-rails) testing framework.
  * [FactoryBot](https://github.com/thoughtbot/factory_bot) for factories.
  * [Webmock (not VCR)](https://github.com/bblimke/webmock) to mock external HTTP requests.
  * [Capybara](https://github.com/teamcapybara/capybara) for integration tests.
* Dev
  * [Better\_errors](https://github.com/BetterErrors/better_errors) for error enhancements.
  * [Pry + pry-rails](https://github.com/pry/pry) for a better console.
  * [Pry-byebug](https://github.com/deivid-rodriguez/pry-byebug) for console debugging.
  * [Bullet](https://github.com/flyerhzm/bullet) to detect N+1 queries.
  * [PgHero](https://github.com/ankane/pghero) for database insights.


# Our Rails ActiveRecord guide

## Retrieving single records

There are three ways to retrieve a single record matching certain criteria. The method to use depends on whether we want it to raise an exception if not found or if we want to find by the primary key or other attributes.

### Retrieve by primary key

Retrieving a record by primary key is the most common scenario. We generally do that in controller methods and background jobs.

If we want the retrieval to **raise an exception** we use `find`. Example:

```
Post.find(params[:id]) # Raises ActiveRecord::NotFound error if the record does not exist
```

We need to be careful when using this form as it can make a request crash. Normally, we use this form in controller methods because we want the request to return a 404 Not Found error and in background jobs to retry the job. In any other case, we should evaluate if it's really a good idea to raise an exception.

If we want the retrieval to **not raise an exception** we use `find_by`. Example:

```
Post.find_by(id: params[:id]) # Returns nil if the record does not exist
```

This is useful to handle cases when a record may or may not exist.

#### Performance

In any of the forms, since we are searching by ID the query will always be fast, as long as the index on the primary key has not deliberately been deleted. The performance of a this kind of queries is always very good.

### Retrieve by other attributes

Retrieving a record by a non-primary key attribute is quite common.

We may want to find a post by a slug, author, tag, etc. When more than one record matches the condition, only the first one is returned (in general, it will be the most recent record).

If we want the retrieval to **raise an exception** we use `find_by!`. Example:

```
Post.find_by!(slug: params[:slug]) # Raises ActiveRecord::NotFound error if the record does not exist
```

Again, we need to be careful when using this form as it can cause a request to crash.

If we want the retrieval to **not raise an exception** we use `find_by`. Example:

```
Post.find_by(slug: params[:slug]) # Returns nil if the record does not exist
```

#### Performance

By default, a query on a table by a condition needs to scan **all records** on the table in order to filter them.

This is OK for small tables like settings-like tables that have the order of 100 records, but it starts to get slower when it gets to bigger orders of magnitude.

To make these queries fast, we need to have **an index** on the combination of attributes we want to retrieve by or at least some of them.

The best way to know if an index works for a query is to `EXPLAIN` the query and check if it uses the index or not.

## Retrieving multiple records

When retrieving a list of records from the database the **most important** thing to always take into account is how many records we are requesting. If we ask for too many records to the database the query will be very slow, it will consume a huge amount of memory and the treatment in Ruby will be slow too.

If not careful **a whole application can be taken down by a single query requesting too much data**.

To retrieve a list of records from the database we use `where`. Example:

```
Post.where(category: params[:category]).limit(50)
```

When doing a query like this in the context of a web request, we need to always limit the returned results by using `limit`. Usually this is done with a pagination library. If there is no pagination involved, we still need to limit the query to a reasonable number.

When we absolutely need to retrieve a large number of records to treat them, we need to use `find_each` and ideally do it outside of a request to avoid having a high response time and a potential (highly probable) timeout.

Usually, we would do it in a background job. The `find_each` method asks for all the records to the database in batches of 1000 by default (it can be configured with the `batch_size` option, like: `find_each(batch_size: 100)`), so that the database load is kept constant and only a limited number of records are retrieved every time.

This also controls memory as only N number of records are kept in memory at a time.

Example:

```
Post.where(created_at: 1.day.ago).find_each do |post|
   post.destroy
end
```

**It's absolutely forbidden to use `all` in any context.** For example: `Post.all`. The only exception is we know 100% sure that a table will always have a very limited set of records.

### Scopes and query execution

It's crucial to understand when a query is executed by Rails and how we can control it. To put it simply: **a query is not executed until its data is needed**.

It's very hard to list all the possible ways when this happens, but these are the most common:

* The results of the query need to be printed to stdout (when using a rails console, for example).
* The results of the query need to be shown on the page.
* An Enumerable method is called on the query to perform some treatment or transformation on the data.
* An aggregation method like `count`, `min` or `max` is called.

The best way to see when a particular expression gets executed and how is to look at the log and see the exact query that gets sent to the database.

#### Examples

If we have a console session open and we do `Post.where(created_at: 1.day.ago)` it will immediately execute the query since the console will request the results to be printed.

If we have in a controller a query like `@posts = Post.where(created_at: 1.day.ago)`, it won't be executed at this time. If we then do this in the view: `@posts.each do |post| ... end` it will execute the query at that time to be able to iterate over the records.

In a controller we can do something like this:

```
def index
  @posts = Post.where(created_at: 1.day.ago)
  filter_posts
end

def filter_posts
  @posts = @posts.where(author: params[:author]) if params[:author]
  @posts = @posts.publishd if params[:published]
end
```

The query will be created incrementally but executed **only once** combining all conditions once the data is requested (usually in the view).

If we do something like this:

```
def index
   @posts = Post.where(created_at: 1.day.ago)
   @posts = @posts.map do |post| # Executes the query!
     ...
   end
end
```

This will execute the query in order to do the `map` because the `map` method needs to have the values in order to transform them.

### Performance

The argument is similar to finding a record by a non-primary key attribute. By default, a query to retrieve a list of results will need to scan through all of the table. These queries can be improved by having indexes on the filtered attributes.

## Joining tables

A common technique used to build more complex queries is to join various tables together through their associations. This allows filtering a table by an attribute of an associated table, order by that attribute, etc.

To join tables we use the `joins` method. Example:

```
Post.joins(:author)
```

We can use attributes from the joined table to filter the query or order it. Examples:

```
Post.joins(:author).where(authors: { gender: :female })
Post.joins(:author).order(birth_date: :desc)
```

Note that inside the where and order clauses we use `authors` (plural). This is because we need to specify the name of the table instead of the association.

This is also relevant when using custom association names in which the name of the association is different from the name of the table.

An example:

```
class User; end
class Post
   belongs_to :author, class_name: 'User'

   scope :from_female_authors ->() { joins(:author).where(users: { gender: :female })
end
```

Joins can traverse as many tables as we want by nesting hashes inside hashes.

For example:

```
Post.joins(author: { address: :city }).where(cities: { country: 'ES' }) # Post -> Author -> Address -> City
```

Often, though, these queries can be simplified why using `has_many through:` associations in the model which end up producing the same queries.

### How joining works at the database level

At a high level a join between table A and table B works like this:

1. It creates a new *virtual table* combining all the columns of table A and all the columns of table B.
2. For every record in table A, it takes all the records in table B that match the `ON` criteria. By default, this matches the primary key of table B with the association foreign key in table A. For each matched record in B, it adds a new row to the *virtual table* with the values of the record in A and the values of the record in B.

It's much easier to see with an example:

Say we have the Post table with the `title` and `author_id` columns, and the Author table with the `name` and `gender` columns.

Posts has the following data:

```
id|title      |author_id
1 |Hello World|1
2 |Bye World  |2
3 |Ruby World |2
```

Authors has the following data:

```
id|name  |gender
1 |Rocky |male
2 |Adrian|female
```

Running `Post.joins(:author).where(authors: { gender: :female }` will produce the following SQL:

```
SELECT * FROM posts INNER JOIN authors ON posts.author_id = authors.id WHERE authors.gender = 'female'
```

Which will create the following virtual relation as a result:

```
posts.id|posts.title|posts.author_id|authors.id|authors.name|authors.gender
2       |Bye World  |2              |2         |Adrian      |female
3       |Ruby World |2              |2         |Adrian      |female
```

Another example: `Author.joins(:posts)` will produce:

```
SELECT * FROM authors INNER JOIN posts ON authors.id = posts.author_id
```

Which will create the following virtual relation as a result:

```
authors.id|authors.name|authors.gender|posts.id|posts.title|posts.author_id
1         |Rocky       |male          |1       |Hello World|1
2         |Adrian      |female        |2       |Bye World  |2              
2         |Adrian      |female        |3       |Ruby World  |2              
```

### Handling repeated values

A common pitfall when working with joins is forgetting to call distinct to remove duplicate values.

Continuing with the example above, suppose we want to get all authors that have posts in the ruby category. We can write this query: `Author.joins(:posts).where(posts: { category: :ruby })`.

However, if we iterate on the results of this query we will find that authors are duplicated **when the author has more than one post in the ruby category**. Specifically, every author will appear N times, where N is the number of ruby posts of the author.

To remove duplicates we need to call `distinct`. Example: `Author.joins(:posts).where(posts: { category: :ruby }).distinct`

Note that this only happens when joining on a `has_many` association, because for `belongs_to` and `has_one` there is always only one (at most) matching record in the joined table.

## Avoiding N+1 queries

It is very easy to introduce N+1 queries when listing records. Example:

```
@posts = Posts.published.limit(50)
@posts.each do |post|
  puts post.author.name
end
```

This will run a query to get all the posts from the database and then, for each post, it will run another query to load the author from the database, for a total of 51 queries.

We can usually use `includes` to avoid the problem. Following the example we could fix the problem by changing the query to `@posts = Posts.published.includes(:author).limit(50)`.

Doing it this way, Rails will only run 2 queries: one to get all the posts and one to get all the authors (by combining the author\_id of each post in a single query).

### Complex N+1 queries

There are times where is not as easy as adding an includes, for example when adding conditions to the associated models. Example:

```
@authors = Author.all.limit(50)
@authors.each do |author|
  post = @author.posts.published.first
  post.title
end
```

In this example, we want to only load published posts for each author, instead of all posts. In order to remove the N+1 query in this scenario we can define a different association in the model with a scope, like this:

```
class Author < ApplicationRecord
  has_many :published_posts, -> { published }, class_name: 'Post', inverse_of: :author
end
```

And now we can use `includes` as normal with this association:

```
@authors = Author.includes(:published_posts).limit(50)
@authors.each do |author|
  post = @author.published_posts.first
  post.title
end
```

Note that in the loop we need to use `@author.published_posts`. If we use `@author.posts.published` it will still trigger the N+1 queries, because Active Record will treat it as a different set of records (Active Record does not know that `published_posts` is the same as `posts.published`).

## Aggregation functions

We need to be careful when running queries that include aggregation functions because these are very hard (often impossible) to optimise by the database engine and often require a full scan of the table.

Running an aggregation function, especially a count, on a big table can have a considerable negative impact on the performance of an application.

When running aggregation queries it should always be by adding conditions that limit the scope of the results, so the full scan only needs to be done from the returned results.

**Protip:** Use `size` instead of `count` unless you are doing a direct count on a table. Using `count` always triggers a query while using `size` is able to use the cached values of a previous query. Example: `Post.published.size` instead of `Post.published.count`.


# Some of our used programming patterns

A collection of MarsBased programming patterns in different languages. For now, only Ruby on Rails.

## Ruby on Rails

* [Presenter](broken://pages/VUA6rXzW48Ic2HDcbt7m)
* [Command](broken://pages/NVmPKyFHxF2kHiq1Ectv)
* [Form Composition](broken://pages/IOqpy4upKohnDhMxo5WO)
* [Query Object](broken://pages/KxAacGzLcE2ZmeRv02h9)


# Our schema implementation guidelines

Schema.org structured data helps search engines and other systems understand what a page is about. Done well, it improves clarity, supports eligibility for rich results where supported, and reduces ambiguity for entity-based retrieval. As such, we think it's critical to any public-facing project we develop to correctly implement this.

This is a practical, developer-focused guide for how we implement and maintain schema.org at MarsBased.

## Principles

1. **Mark up what the user can see:** Structured data must reflect the visible, user-facing content of the page. If the markup claims things the page does not show, it is a liability (validation issues, manual actions, or simply ignored markup). There are a few exceptions, which we will cover later.
2. **Prefer JSON-LD:** We use JSON-LD because it is easier to generate, test, and maintain than Microdata embedded across templates. We stopped putting schema in Microdata the moment we had to debug it across templates because it's very easy to skim through HTML and not see some fields and schema.org annotations you wanted to modify or get rid of.
3. **Think in entities and relationships, not fields:** A page is not "a blob of properties". It is typically a small graph: Organization -> WebSite -> WebPage -> mainEntity (Article, Service, Product, JobPosting, etc).
4. **Use stable IDs to connect nodes:** Use "@id" to link nodes across the graph. This prevents duplicate entities and keeps the markup coherent as the site grows.
5. **Keep it minimal, correct, and testable:** More markup is not automatically better. Aim for the smallest graph that is accurate, consistent, and validated.

## What we implement by default

Most sites we build should implement these baseline nodes, at least.

### 1) Organisation (site-wide)

Used to define the publisher and the entity behind the website. In the case of our website, MarsBased is our organisation.

Recommended:

* "@type": "Organization" (or "LocalBusiness" if truly local-first)
* "name"
* "url"
* "logo" (as an ImageObject if possible)
* "sameAs" (only real profiles - like social media)
* "contactPoint" (optional but useful)

### 2) WebSite (site-wide)

Defines the website as an entity.

Recommended:

* "@type": "WebSite"
* "@id"
* "url"
* "name"

Optional:

* "potentialAction" with "SearchAction" if the site has a real on-site search.

### 3) WebPage (per page)

Represents the page URL and ties it to the primary thing described.

Recommended:

* "@type": "WebPage"
* "@id"
* "url"
* "name"
* "isPartOf": WebSite "@id"
* "about" or "mainEntity": the primary entity of the page (see below)

### 4) BreadcrumbList (per page, if breadcrumbs exist)

Only add it if breadcrumbs exist in the UI. CMSs (Content Management Systems) often make use of breadcrumbs in their UIs.

## Page-type guidelines (pick the primary entity)

Each page should have a clear primary entity in structured data.

### Article / BlogPosting (content pages)

Use for posts, guides, case studies, and editorial content.

Recommended:

* "@type": "BlogPosting" or "Article"
* "headline"
* "description" (should match the page summary)
* "image" (URL or ImageObject)
* "author" (Person or Organization)
* "publisher" (Organization "@id")
* "datePublished"
* "dateModified"
* "mainEntityOfPage": WebPage "@id"

Notes:

* Always update "dateModified" when content changes materially. For instance, if you edit a blog post, your CMS should automatically do it.
* Keep author data consistent site-wide (same name, same "@id").

### Service (service landing pages)

Use for agency service pages describing what we provide. In our case, every page under <https://marsbased.com/services>

Recommended:

* "@type": "Service"
* "name"
* "description"
* "provider": Organization "@id"
* "areaServed" (only where true)
* "serviceType" (optional)
* "offers" (optional, only if pricing is actually shown)

Notes:

* Avoid inventing prices in schema if the page does not show them.

### Product / SoftwareApplication (product pages)

Use if a page describes a sellable product or software. Critical for e-commerce sites.

Recommended for Product:

* "@type": "Product"
* "name"
* "description"
* "image"
* "brand" (if applicable)
* "offers" (only if price/availability is shown)

Recommended for SoftwareApplication:

* "@type": "SoftwareApplication"
* "name"
* "applicationCategory"
* "operatingSystem" (if relevant)
* "offers" (only if price is shown)

Notes:

* If you cannot support key commerce fields with real page content, do not force Product markup.

### JobPosting (careers)

Use for job pages.

Recommended:

* "@type": "JobPosting"
* "title"
* "description" (can be the main job body)
* "hiringOrganization": Organization "@id"
* "datePosted"
* "validThrough" (important for stale listings)
* "employmentType"
* "jobLocationType" for remote roles
* "applicantLocationRequirements" for remote eligibility (country/region)
* "directApply" (only if it truly applies)

Notes:

* Keep job structured data aligned with the current job status. Expired roles should be removed or closed via the page content and schema.
* The location requirements content type have changed a few times over the years. Use a validator to ensure you're using the correct format/entity.

### Other entities

"FAQPage" and "HowTo" entities are becoming more and more popular, in line with the conversational kind of content preferred by LLMs and crawlers of companies like Perplexity, ChatGPT, Claude and the like.

"Person", although is only mentioned in passing for author @id, it deserves a good mention since companies like MarsBased publish a lot of authored content (blog, case studies, podcasts, news). Having a profile for each person in the company helps to build their profile and rank better in authority. Related to that, paginated content (blog index pages, etc.) they get "WebPage" with a "CollectionPage" type. Same for Authors and the typical Author page like: <https://marsbased.com/blog/Anna-Vidal>

Last, but not least, "Event". if you're building community stuff or clients have events, this comes up. Don't have events on our website but when we will incorporate this, sometime in the future, it'll be important.

## Implementation patterns we prefer

### One JSON-LD block per page, using @graph

The script approach is the one we go for as we can't see a reason to implement the other approaches. Placing it in the is conventional, but render-blocking concerns on JS-heavy sites sometimes push it to end of . We personally prefer the latter.

We generate a single script tag with an "@graph" that includes:

* Organization
* WebSite
* WebPage
* BreadcrumbList (if applicable)
* Primary entity (Article/Service/etc)

Example (applicantLocationRequirements cut for legibility purposes):

```
<script type="application/ld+json">
  { "@context":"https://schema.org/",
      "@graph": [
        {
        "@type":"JobPosting",
          "title":"Full Stack Software Engineer  (100% Remote)",
          "description":"We are looking for a Full Stack Software Engineer to join our Martian team.",
          "identifier":{"@type":"PropertyValue","name":"MarsBased","value":""},
          "datePosted":"2026-02-06",
          "applicantLocationRequirements":[{"@type":"Country","name":"Austria"},{"@type":"Country","name":"Belgium"},[...]],
          "jobLocationType":"TELECOMMUTE",
          "employmentType":"FULL_TIME",
          "hiringOrganization":{"@id":"https://marsbased.com/#organization"},
          "baseSalary":{"@type":"MonetaryAmount","currency":"EUR","value": {"@type":"QuantitativeValue","minValue":40000,"maxValue":60000,"unitText":"YEAR"}}}
              ]
    }
</script>
```

This makes QA and diffing simpler and avoids scattering markup across components. We don't want littered HTML with bits of schema here and there.

### Stable "@id" scheme

Pick a consistent convention:

* Organization: "<https://example.com/#organization>"
* WebSite: "<https://example.com/#website>"
* WebPage: page URL plus "#webpage" or similar
* People: "<https://example.com/#person-jane-doe>" (or author page URL)

If an entity has its own canonical URL (author page, product page), prefer that as its "@id".

### Centralize data sources

Avoid hardcoding schema objects in multiple templates. Instead:

* Maintain a single source of truth for Organization and WebSite data.
* Derive WebPage and page entity markup from page metadata (CMS, frontmatter, DB).

### Multilingual sites

Multilingual sites introduce a specific problem: the same content exists at multiple URLs, and structured data must not treat those URLs as separate entities. The goal is one coherent entity graph, with language variants correctly signalled; not three disconnected graphs that happen to describe the same thing.

**The core rule:** **canonical @id, language-specific @id only where justified**. The "Organization" and "WebSite" nodes are language-agnostic. They represent the entity itself, not a page about the entity. Use the canonical (language-neutral) URL as the @id and do not duplicate or translate these nodes per language variant.

```json
{
  "@type": "Organization",
  "@id": "https://example.com/#organization",
  "name": "Example Company",
  "url": "https://example.com"
}
```

This node is identical across all language variants. It goes in every @graph, unchanged. The "WebPage" node is where language variants diverge. Each URL gets its own "WebPage" node with its own @id because each URL genuinely is a different page, but all of them point back to the same "Organization" and "WebSite" via "isPartOf" and "publisher".

```json
{
  "@type": "WebPage",
  "@id": "https://example.com/ca/about/#webpage",
  "url": "https://example.com/ca/about/",
  "name": "Sobre nosaltres",
  "inLanguage": "ca",
  "isPartOf": { "@id": "https://example.com/#website" }
}
```

**Use inLanguage on content nodes**

Add "inLanguage" to "WebPage", "BlogPosting", "Article", and any content entity that has a language-specific expression. Use BCP 47 codes: "ca" for Catalan, "es" for Spanish, "en" for English, just to name a few.

Do not add "inLanguage" to "Organization" or "WebSite". Those entities have no language.

**Connect variants with sameAs or translationOfWork**

If the page has a clear canonical equivalent, you can signal the relationship between translated pages using "translationOfWork" on the translated version pointing to the canonical:

```json
{
  "@type": "BlogPosting",
  "@id": "https://example.com/ca/blog/some-post/#blogposting",
  "inLanguage": "ca",
  "translationOfWork": {
    "@id": "https://example.com/blog/some-post/#blogposting"
  }
}
```

This is optional but useful for content-heavy sites. Do not use "sameAs" here. "sameAs" is for external identity claims (social profiles, Wikidata entries), not for relating internal URL variants.

**hreflang is not schema — but it matters just as much**

Language variant signalling for crawlers happens primarily through hreflang, not structured data. Make sure hreflang annotations are in place (via tags in or HTTP headers) before worrying about language in schema. Schema inLanguage is additive context; hreflang is what tells Google which URL to serve to which user. If hreflang is wrong or missing, schema "inLanguage" will not compensate for it.

**Common mistakes on multilingual sites**

Duplicating the "Organization" node in each language with a translated name. The legal entity name does not change, as "name" should be the registered name, not a translated label.

Using the canonical URL as the @id for translated "WebPage" nodes. Each page URL must be its own @id. The canonical URL belongs in a "url" or "mainEntityOfPage" reference, not as the @id of a different page's node.

Omitting "inLanguage" entirely. Crawlers can infer it from hreflang, but being explicit in schema removes ambiguity, especially as LLM-based retrieval systems grow more prevalent.

Generating identical JSON-LD across all language variants. If the only thing changing between the Catalan and Spanish page schemas is the @id URL but not "inLanguage" or content properties like headline, the markup is incomplete.

## Validation and QA checklist

We do not ship structured data without a basic QA pass. We test using the Schema Markup Validator (schema.org/validator) which catches type/property errors the Google tool misses.

1. Syntax and shape validation

* Validate the JSON-LD is valid JSON.
* Validate schema types and property names.

2. Eligibility testing (Google-focused)

* If the page targets a rich result type, test with the Rich Results Test.
* Monitor Search Console enhancement reports after deploy.

3. Content parity

* Confirm every important claim in markup is visible on the page.
* Confirm dates, prices, availability, and locations match.

4. Regression control

* Add snapshot tests for JSON-LD generation where feasible.
* Include a structured data smoke test in release QA for critical templates.

## Common mistakes to avoid

* Marking up content that is not on the page (or only behind UI interactions).
  * **Exception:** When a certain entity requires a field that you don't want to show in the UI, just add it as a meta tag.
* Copy-pasting generic schemas that do not match the page.
* Missing "@id" links so entities become disconnected.
* Using Product/Offer fields without showing pricing/availability in the UI.
* Leaving JobPosting live after the role is closed.
* Duplicating Organization definitions with different names/URLs across pages.

## Minimal example structure (conceptual)

* Organization "@id": "<https://marsbased.com/#organization>"
* WebSite "@id": "<https://marsbased.com/#website>"
* WebPage "@id": "<https://marsbased.com/blog/some-post/#webpage>"
* BlogPosting "@id": "<https://marsbased.com/blog/some-post/#blogposting>"
* BlogPosting "publisher": Organization "@id"
* WebPage "mainEntity": BlogPosting "@id"

Keep it boring and correct. That is what scales.


# Accessibility guidelines

Accessibility is a core part of building high-quality digital products. These guidelines are not a full accessibility manual, but a practical reference with simple, high-impact practices that designers and frontend engineers at MarsBased can apply in their daily work.

The goal is to help us:

* design and build interfaces that more people can use
* avoid common accessibility pitfalls early, before implementation
* ensure consistency when collaborating with clients and external designers
* follow industry-standard best practices without overcomplicating workflows

This guide is intentionally lightweight and opinionated. It focuses on what we can implement easily in our projects, from basic contrast checks to semantic HTML, keyboard navigation, and accessible component patterns.

Every improvement here benefits real users in real conditions: people with permanent, temporary, or situational limitations, but also search engines, voice assistants, and any system that needs to understand our interfaces.

According to the [Centers for Disease Control and Prevention](https://www.cdc.gov/disability-and-health/articles-documents/disabilities-health-care-access.html), 1 in 4 adults lives with some form of disability. Accessibility isn't about edge cases, it's about designing for a significant portion of your users.

Accessibility is not extra work. It's part of building things well.

## Table of Contents

* [1. Accessibility for Designers](/our-accessibility-guides/for-designers)
  * [1.1. Color & Contrast](/our-accessibility-guides/for-designers#ColorContrast)
  * [1.2. Typography & Legibility](/our-accessibility-guides/for-designers#TypographyLegibility)
  * [1.3. Layout & Cognitive Load](/our-accessibility-guides/for-designers#LayoutCognitiveLoad)
  * [1.4. Motion & Reduced Motion](/our-accessibility-guides/for-designers#MotionReducedMotion)
  * [1.5. Touch Targets](/our-accessibility-guides/for-designers#TouchTargets)
  * [1.6. Handoff for Accessible Implementation](/our-accessibility-guides/for-designers#HandoffforAccessibleImplementation)
* [2. Accessibility for Frontend Engineers](/our-accessibility-guides/for-engineers)
  * [2.1. Semantic HTML First](/our-accessibility-guides/for-engineers#SemanticHTMLFirst)
  * [2.2. Labels & Forms](/our-accessibility-guides/for-engineers#LabelsForms)
  * [2.3. Keyboard Navigation](/our-accessibility-guides/for-engineers#KeyboardNavigation)
  * [2.4. Images & Alt Text](/our-accessibility-guides/for-engineers#ImagesAltText)
  * [2.5. ARIA](/our-accessibility-guides/for-engineers#ARIA)
  * [2.6. Custom UI Components](/our-accessibility-guides/for-engineers#CustomUIComponents)
  * [2.7. Reduced Motion](/our-accessibility-guides/for-engineers#ReducedMotion)
  * [2.8. Basic Accessibility Testing](/our-accessibility-guides/for-engineers#BasicAccessibilityTesting)
* [3. Accessibility Quick Checklist](#AccessibilityQuickChecklist)
  * [3.1. For Designers](/our-accessibility-guides/for-designers#DesignAccessibilityChecklist)
  * [3.2. For Engineers](/our-accessibility-guides/for-engineers#EngineeringAccessibilityChecklist)
* [4. Resources](/our-accessibility-guides/resources)

## 1. Accessibility for Designers

Accessibility starts at design time, not at the implementation stage. Design decisions determine whether a component can ever be made accessible. Engineers can solve some issues, but not all (e.g., low contrast, overly complex layouts).

[Read the full guide for designers →](/our-accessibility-guides/for-designers)

## 2. Accessibility for Frontend Engineers

Many accessibility issues arise during implementation, but most disappear when we use the web platform as intended. Start with semantic HTML. Add ARIA only when necessary. Don't remove built-in accessibility.

[Read the full guide for engineers →](/our-accessibility-guides/for-engineers)

## 3.Accessibility Quick Checklist <a href="#accessibilityquickchecklist" id="accessibilityquickchecklist"></a>

A fast, practical checklist you can use before handing off designs or shipping frontend work.

* [Checklist for Designers →](/our-accessibility-guides/for-designers#DesignAccessibilityChecklist)
* [Checklist for Engineers →](/our-accessibility-guides/for-engineers#EngineeringAccessibilityChecklist)

## 4. Resources

A curated list of reliable, practical resources to go deeper into accessibility.

[View all resources →](/our-accessibility-guides/resources)


# Accessibility for designers

Accessibility starts at design time, not at the implementation stage.

* Design decisions influence whether a component can ever be made accessible.
* Engineers can fix some issues, but not all (e.g., insufficient contrast, overly complex layouts).
* Treat accessibility as a shared responsibility across the team: designers provide accessible structures, and engineers implement them robustly.

The following sections outline the core areas designers can influence the most.

## Table of Contents

* [1. Color & Contrast](#ColorContrast)
* [2. Typography & Legibility](#TypographyLegibility)
* [3. Layout & Cognitive Load](#LayoutCognitiveLoad)
* [4. Motion & Reduced Motion](#MotionReducedMotion)
* [5. Touch Targets](#TouchTargets)
* [6. Handoff for Accessible Implementation](#HandoffforAccessibleImplementation)
* [7. Design Accessibility Checklist](#DesignAccessibilityChecklist)

## 1.Color & Contrast <a href="#colorcontrast" id="colorcontrast"></a>

Color is a critical part of accessible design. Many users rely on sufficient contrast and clear visual distinctions to understand content, navigate interfaces, and identify actions. Poor contrast affects people with visual impairments, color blindness, or ageing vision, but also users in bright environments or using low-quality displays.

### Minimum contrast ratios

Follow the WCAG contrast ratios for text and interactive elements:

* **4.5:1** for regular text
* **3:1** for large text (18px regular, or 14px bold)
* **3:1** for UI components and graphical objects (icons, inputs, borders)

Use a contrast checker during the design process. Avoid relying on #999, #AAA or similarly light greys for essential text.

### Do not rely on color alone

Color must not be the only indicator of meaning.\
For example, error messages should not be communicated only using red. Always pair color with another cue:

* an icon
* a pattern
* a label
* bold text
* a border style

This ensures users with color blindness or low vision can understand the information.

### Consistent and accessible color tokens

When defining or reviewing a design system:

* Ensure color tokens (`primary`, `danger`, `success`, etc.) meet contrast requirements by default.
* Provide accessible variants for light and dark mode.
* Document intended usage so engineers can apply colors consistently.

### Testing color contrast early

Identify issues before implementation by testing:

* text over backgrounds
* interactive elements in all states (hover, active, disabled)
* text over gradients or images
* components in both light and dark themes

Chrome DevTools and tools like [WebAIM](https://webaim.org/resources/) or [Stark](https://www.getstark.co/) make this process straightforward.

## 2.Typography & Legibility <a href="#typographylegibility" id="typographylegibility"></a>

Good typography is essential for accessibility. It ensures users can read, scan and understand content without unnecessary effort. This applies to everyone: people with low vision, dyslexia, attention difficulties, or simply reading on a small/mobile screen.

#### Font size

* Use a **minimum of 16px** for body text.
* Larger text (18–20px) improves readability on mobile and for long-form content.
* Avoid locking text sizes inside fixed components (modals, cards); let them scale with user preferences.

#### Line height & spacing

* Use **1.4–1.6** line-height for paragraphs.
* Keep line spacing consistent for predictable rhythm.
* Provide adequate spacing between paragraphs and headings; avoid cramped blocks of text.

#### Line length

* Aim for **45–75 characters per line** on desktop for optimal readability and to avoid excessive line breaks.
* Allow text to reflow naturally on mobile. Don't force fixed container widths.

#### Typeface choices

* Prefer simple, readable sans-serif fonts for UI.
* Avoid ultra-light, condensed or decorative typefaces for body text.
* Check that the chosen font renders well in multiple languages (accents, special characters).

#### Text weight & emphasis

* Use **font-weight 400–600** for most UI text.
* Avoid relying solely on color for emphasis (pair it with bold, underline or patterns).
* Use italics sparingly; they reduce legibility, especially on low-resolution screens.

#### When designing text over background images

* Ensure strong contrast between text and background.
* Add overlays or scrims when necessary; don't rely on "hopefully the image is dark enough".
* Test multiple image variations if the content is dynamic.

#### Responsive text behavior

* Avoid pixel-perfect typography specs that break when the viewport changes.
* Allow accessible zooming: users must be able to scale text up to **200%** without breaking layout.
* Avoid fixed heights on text containers.

## 3.Layout & Cognitive Load <a href="#layoutcognitiveload" id="layoutcognitiveload"></a>

Accessible layouts help users understand information quickly and navigate without confusion. This matters particularly for people with cognitive disabilities, ADHD, dyslexia, or anyone dealing with fatigue, stress, or a noisy environment.

#### Visual hierarchy & organization

* Use clear hierarchy (headings, subheadings, body, captions) and ensure one main focal point per screen.
* Group related content together; separate sections with spacing, dividers or background changes.
* Use predictable patterns: consistent card structures, consistent placement of actions, consistent navigation across the product.
* Avoid sudden layout shifts or reorganizing key sections between screens.

#### Reduce cognitive load

* Minimize competing elements and visual noise (too many icons, colors, borders, animations).
* Use whitespace strategically; it improves comprehension and helps users focus.
* Prefer single-column layouts for text-heavy content (forms, articles, instructions).
* Avoid long uninterrupted blocks of text or UI elements.

#### Clear content & language

* Use short sentences, straightforward language, and avoid unnecessary jargon.
* Break complex tasks into smaller steps with step-by-step flows.
* Maintain consistent labeling ("Submit" shouldn't become "Go!" elsewhere).
* Provide clear error messages that explain both the problem and how to fix it.
* Make interactive elements clearly interactive through styling, affordances and consistent focus/hover states.

## 4.Motion & Reduced Motion <a href="#motionreducedmotion" id="motionreducedmotion"></a>

Motion can enhance clarity, but it can also cause distraction, disorientation or even physical discomfort for some users (motion sensitivity, vestibular disorders, migraines, ADHD). Accessible motion means using animation intentionally and providing a way to reduce it.

#### Use motion with purpose

* Use animation to clarify, not to decorate.
* Prefer small, meaningful transitions: fade, scale, slide with subtle easing.
* Avoid motion that feels abrupt, fast or directionally intense.

#### Respect the user's "prefers-reduced-motion" setting

* When enabled, remove or drastically simplify animations.
* Replace long or looping motion with instant or minimal alternatives.
* Apply it to micro-interactions, page transitions and any auto-moving content (carousels, auto-scrolling, background motion).

#### Avoid problematic movement

* Avoid parallax and continuous background animations.
* Avoid scroll-hijacking.
* Avoid large element movements that slide big chunks of the UI across the screen.
* Be especially careful with motion that feels like camera movement (zooming, panning, spinning).

#### Timing and pacing

* Keep transitions short (150–250ms for most UI). Longer animations can cause discomfort.
* For complex motions, ensure they can be interrupted or skipped.
* Don't rely on animation to convey essential information.

## 5.Touch Targets <a href="#touchtargets" id="touchtargets"></a>

Touch interactions must be easy, forgiving and reliable, especially on mobile. Small or tightly packed targets are one of the most common accessibility failures, affecting users with motor impairments, tremors, larger fingers, or simply using the phone while walking or holding something.

#### Minimum target size & spacing

* Aim for **44×44px** minimum ([Apple](https://developer.apple.com/design/human-interface-guidelines/accessibility#Mobility)) or **48×48px** ([Material Design](https://m3.material.io/foundations/designing/structure#dab862b1-e042-4c40-b680-b484b9f077f6)) for **all** tappable elements: buttons, icons, toggles, menu items, close icons, etc.
* The visible icon can be smaller, the tappable area should not.
* Leave enough separation between interactive elements to avoid accidental taps.
* Avoid placing small actions too close to destructive ones (e.g., "Delete" next to "Edit" without spacing).
* At least 8px spacing between touch elements is recommended.

#### Make actions clear

* Pair icons with text when clarity matters. Text improves accuracy for users with motor issues and reduces ambiguity.
* Don't hide primary actions behind long-press gestures or tiny affordances.
* Make interactive elements look interactive through shape, color, and clear states.
* Provide clear visual feedback: pressed, active and disabled states.
* Avoid animations that move the target during the tap (shifting position makes selection harder).

## 6.Handoff for Accessible Implementation <a href="#handoffforaccessibleimplementation" id="handoffforaccessibleimplementation"></a>

When delivering designs, include the essential information engineers need to implement accessibility correctly.

#### Communicate semantic intent

* Indicate which elements are buttons, links, headings, lists or form fields.
* Mark which icons are decorative and which convey meaning.

#### Labels, alt text & forms

* Provide the **visible label** for every form field. Visually hidden labels are fine for simple fields (e.g., search), but **long forms require visible labels** for clarity and faster scanning.
* Supply the **intended alt text** for meaningful images.
* Include examples of **error states** for each field type: error message text, placement, and when it should appear.
* Define the **expected keyboard order** (Tab flow) for complex forms or multi-step flows to avoid confusion.

#### States & interactions

* Deliver the basic interaction states: focus, active, hover, disabled, error.
* For modals, dropdowns or other complex components, clarify how they open and close.
* Provide keyboard behavior expectations.

#### Color & spacing

* Confirm that color choices meet contrast requirements.
* Ensure interactive elements meet minimum target size and spacing.

## 7.Design Accessibility Checklist <a href="#designaccessibilitychecklist" id="designaccessibilitychecklist"></a>

A fast, practical checklist to review designs before shipping.

* **Color & contrast** meet WCAG AA (4.5:1 for text, 3:1 for large text).
* **Text size** is readable (16px+ body, avoid ultra-light fonts).
* **Clear hierarchy**: headings, spacing and grouping guide the eye.
* **Minimal cognitive load**: simple layouts, predictable patterns.
* **Reduced motion** considerations: avoid parallax and heavy animations.
* **Touch targets** are 44–48px minimum.
* **Interactive elements look interactive** (clear states, labels, icons).
* **Forms have visible labels**, not placeholders.
* **Images have context**: decorative vs informative is clear.
* **Interactive states**: provide hover, focus and active states in design handoff.


# Accessibility for engineers

Many accessibility issues arise during implementation, but the good news is that most disappear when we use the web platform as intended.

Your default mindset should be:

**Start with semantic HTML. Add ARIA only when necessary. Never remove built-in accessibility.**

Small decisions in markup have a huge impact: keyboard navigation, screen reader output, SEO, machine readability and maintainability all depend on correct semantics.

## Table of Contents

* [Semantic HTML First](#SemanticHTMLFirst)
* [Labels & Forms](#LabelsForms)
* [Keyboard Navigation](#KeyboardNavigation)
* [Images & Alt Text](#ImagesAltText)
* [ARIA](#ARIA)
* [Custom UI Components](#CustomUIComponents)
* [Reduced Motion](#ReducedMotion)
* [Basic Accessibility Testing](#BasicAccessibilityTesting)
* [Engineering Accessibility Checklist](#EngineeringAccessibilityChecklist)

## Semantic HTML First <a href="#semantichtmlfirst" id="semantichtmlfirst"></a>

Native HTML elements come with accessibility built in: roles, keyboard behavior, focus handling, and meaningful semantics. Using them correctly is the easiest and most reliable way to build accessible interfaces.

### Use the right element for the job

* `<button>` for actions
* `<a>` for navigation
* `<form>` and real form controls (`<input>`, `<select>`, `<textarea>`) for data entry
* `<header>`, `<main>`, `<nav>`, `<section>`, `<article>`, `<footer>` for structure
* `<ul>/<ol>/<li>` for lists

This immediately gives you:

* correct semantics for screen readers
* predictable keyboard behavior (Enter, Space, Tab, Escape)
* proper focus management without extra JS
* better SEO and machine readability

### Don't replace native controls with divs

Avoid patterns like:

```html
<div onclick="submitForm()">Send</div>
```

They're not focusable, not operable by keyboard and not announced properly by screen readers.

If you must build a custom component, you'll need to manually add:

* correct `role`
* `tabIndex="0"`
* full keyboard support (`Enter`, `Space`, arrow keys when needed)
* proper focus states
* ARIA attributes where appropriate

This is significantly more work and easier to get wrong.

### Keep your HTML predictable

* Respect the natural heading hierarchy (`h1 → h2 → h3` …).
* Avoid skipping levels (don't jump from `h1` to `h4`).
* Use one `<main>` landmark per page.
* Wrap related controls in a `<fieldset>` with a `<legend>` when appropriate.

Good structure helps not only assistive tech but also users scanning the page visually.

### Bonus: Accessible = Machine-readable

Accessibility isn't just about people; it's about making content understandable to machines too.

Clear semantic HTML is easier for:

* search engines (better SEO)
* voice assistants (Siri, Alexa, Google Assistant)
* browser accessibility APIs
* AI-powered tools and summarizers
* automated testing and maintenance

Good markup benefits **every layer of the ecosystem**, not just screen readers. When you build accessible interfaces, you're also building machine-readable, maintainable code that works better for everyone.

## Labels & Forms <a href="#labelsforms" id="labelsforms"></a>

Forms must be identifiable, operable, and understandable, both visually and with assistive technologies.\
Good markup solves most accessibility issues automatically.

### Forms must be real `<form>` elements

A search bar is still a form and it should use `<form>` and a submit button.\
This improves semantics, keyboard support, and allows assistive tools to trigger the action correctly.

```html
<form action="/search">
  <label for="q" class="sr-only">Search</label>
  <input id="q" name="q" type="search" placeholder="Search…" />
  <button type="submit">Search</button>
</form>
```

### Always provide a real label

* Every form control needs an associated `<label>` that must be programmatically associated with the field:

```html
<label for="email">Email address</label> <input id="email" type="email" />
```

* Labels should be visible and placed consistently (top or left) when possible. **If the UI doesn't allow labels** (e.g., search bars or very short forms), use a visually hidden label:

```html
<label for="search" class="sr-only">Search</label>
<input id="search" type="text" placeholder="Search..." />
```

> **Note:** `.sr-only` (screen-reader-only) is a utility class that visually hides content while keeping it accessible to assistive technologies. Most CSS frameworks include this class. If you need to implement it yourself:

```css
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
```

> Hidden labels are acceptable only in **very short** forms.\
> For login, registration, or any multi-field flow, **visible labels are required**.

* **Never rely on placeholders as labels**. Placeholders disappear on typing, offer poor contrast, and are not consistently announced by screen readers. Use them only for hints or examples (e.g., "<you@example.com>"), never as the main label.

### Required fields

* Mark required fields using the native `required` attribute; this automatically exposes the information to assistive tech.
* When necessary, reinforce with `aria-required="true"` (e.g., in custom components).
* Provide a **clear visual indicator** like "(required)" or "Required".\
  Avoid using a lone asterisk `*` without context. Many users don't understand what it means.

```html
<label for="name">Full name <span aria-hidden="true">(required)</span></label>
<input id="name" name="name" required />
```

### Autocomplete

Autocomplete helps users with cognitive disabilities, dyslexia, ADHD, or memory difficulties by reducing the amount of information they must recall and type.

* Always add `autocomplete` when the field has a known purpose.
* Use specific values such as `email`, `name`, `address-line1`, `tel`, `current-password`, `new-password`, etc.
* This improves speed, accuracy, and reduces form abandonment.

```html
<input type="email" id="email" name="email" autocomplete="email" required />
```

### Disabled fields

* Disabled native inputs are skipped by keyboard navigation and screen readers.
* Avoid disabling fields without explanation; many users won't know why they can't interact.
* When a field is intentionally unavailable, provide context:

```html
<label for="coupon">Coupon</label>
<input id="coupon" disabled aria-describedby="coupon-info" />
<p id="coupon-info">Coupons are not available for this plan.</p>
```

* For custom components, you have two options:
  * Match native behavior: remove from tab order (`tabindex="-1"`) and mark as disabled (`aria-disabled="true"`) so it behaves like a disabled native input.
  * Keep it discoverable: use `aria-disabled="true"` when users need to understand why it's unavailable (especially with explanatory text via `aria-describedby`).

```html
<div
  role="textbox"
  contenteditable="true"
  tabindex="0"
  aria-disabled="true"
  aria-label="Coupon code"
  aria-describedby="coupon-info"
></div>
<p id="coupon-info">Coupons are not available for this plan.</p>
```

### Group related fields

For radios, checkboxes or grouped selections, use `<fieldset>` and `<legend>`:

```html
<fieldset>
  <legend>Payment method</legend>
  <label><input type="radio" name="payment" />Card</label>
  <label><input type="radio" name="payment" />PayPal</label>
</fieldset>
```

### Helpful hints & instructions

* Provide short, actionable instructions near the field ("Must be 8–20 characters").
* Use `aria-describedby` for hints that should be announced:

```html
<input id="username" aria-describedby="username-hint" />
<p id="username-hint">Must be unique and contain only letters or numbers.</p>
```

### Accessible errors

* Errors should describe the actual problem ("Password must be at least 8 characters", not "Invalid input") and be announced to screen readers.
* They should appear near the field, not at the bottom of the form, and be visually distinct with both **color + text** (don't rely on red alone).
* Use `aria-invalid` and `role="alert"` when errors are present, and associate the message via `aria-describedby`.

```html
<label for="email">Email</label>
<input id="email" aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" role="alert">Enter a valid email address</p>
```

### Success messages

Users should be informed, not only visually, when an action succeeded.

* For non-critical updates (e.g., "Saved"), use `role="status"`, which announces the message politely without interrupting screen reader flow.
* For important confirmations (e.g., "Payment complete"), use `role="alert"` to announce it immediately.

```html
<p role="status">Your changes have been saved.</p>

<p role="alert">Your payment was successful.</p>
```

These roles ensure the message is spoken automatically without requiring the user to focus it.

### Predictable keyboard flow

* Users must be able to complete the form with Tab, Shift+Tab, and Enter.
* Maintain logical field order in the DOM. Do **not** rearrange form fields visually via CSS only.
* Avoid trapping focus inside custom widgets unless necessary (e.g., date pickers), and provide a clear escape path.

### Provide focus styles

* Don't remove focus outlines: they're essential for keyboard and low-vision users.
* If you customize focus styles, ensure they're clearly visible (sufficient contrast, adequate size).
* Prefer `:focus-visible` to style only real keyboard focus, leaving mouse focus clean:

```css
button:focus-visible,
input:focus-visible {
  outline: 2px solid #005fcc;
  outline-offset: 2px;
}
```

## Keyboard Navigation <a href="#keyboardnavigation" id="keyboardnavigation"></a>

Keyboard accessibility is essential for users who cannot use a mouse (motor disabilities, repetitive strain injuries, temporary injuries) and for power-users who simply prefer keyboard interaction. If a UI can't be operated with a keyboard, it is not accessible.

### Ensure all interactive elements are reachable

* Links, buttons, inputs, and controls must be focusable with **Tab**.
* Use **semantic elements first**: `<button>`, `<a>`, `<input>`, `<select>`, `<textarea>`.
* If you create a custom interactive component (e.g., `<div>` acting as a button), you **must** add:
  * `tabindex="0"`
  * Keyboard handlers for **Enter** and **Space**
  * A visible focus state

```html
<div
  role="button"
  tabindex="0"
  onkeydown="if (event.key === 'Enter' || event.key === ' ') this.click()"
>
  Custom button
</div>
```

But: **prefer a real `<button>` whenever possible**.

### Logical tab order

* The tab order must follow the visual reading order.
* Avoid large jumps caused by:
  * Absolutely positioned elements
  * Portalled modals without proper focus handling
  * Moving elements into a new DOM position on focus or hover
* Hidden elements (`display:none`, `visibility:hidden`) should not be focusable.

### Visible focus styles

Users must always see **where they are** on the screen.

* Never remove the outline without providing an accessible replacement.
* Prefer `:focus-visible` for modern browsers:

```css
button:focus-visible,
a:focus-visible {
  outline: 3px solid #3b82f6;
  outline-offset: 3px;
}
```

This avoids showing focus on mouse click, but keeps it for keyboard users.

### Focus behavior in complex UI

Some components require explicit focus management:

* **Modals:**
  * Trap focus inside the modal.
  * Return focus to the trigger when the modal closes.
* **Menus and dropdowns:**
  * Move focus to the first menu item when opened.
  * Close on Escape.
* **Tabs:**
  * Arrow keys should navigate between tabs.
  * Tab should move into/out of the tab panel content.

A minimal focus trap example:

```js
const focusable = modal.querySelectorAll("button, a, input, select, textarea");
const first = focusable[0];
const last = focusable[focusable.length - 1];

modal.addEventListener("keydown", (e) => {
  if (e.key !== "Tab") return;

  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault();
    last.focus();
  } else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault();
    first.focus();
  }
});
```

For more complex scenarios, consider using libraries like [focus-trap](https://github.com/focus-trap/focus-trap) or [focus-trap-react](https://github.com/focus-trap/focus-trap-react) for React applications.

### Managing focus responsibly

* Don't move elements into a different DOM position on focus: it breaks tab order.
* If you manually call `.focus()`, make sure it's predictable and not surprising.
* Returning focus to the trigger element after closing overlays improves usability.
* Keep focus out of hidden or collapsed content (`display:none` elements should not be focusable).

### Don't hijack keyboard behavior

* Don't override arrow keys unless you're building a component that traditionally uses them (menus, sliders, carousels).
* Don't trap the user inside components unintentionally (e.g., in carousels or chat windows).
* Avoid global `keydown` listeners that swallow Escape or Tab.

### Quick test

A 10-second test to catch most issues:

1. Put your mouse aside.
2. Press **Tab**.
3. Can you see where you are? (focus indicator)
4. Try to reach all interactive elements.
5. Try to operate the entire flow: open menus, submit forms, close dialogs.

If something can't be done with the keyboard, it's a red flag.

## Images & Alt Text <a href="#imagesalttext" id="imagesalttext"></a>

Images need meaningful text alternatives so assistive technologies can convey their purpose or content. The goal is not to describe the pixels, but to communicate the **intent**.

### When an image conveys information

Provide a short, specific description that reflects what the user needs to understand.

```html
<img
  src="team-photo.jpg"
  alt="The MarsBased team standing in front of the office"
/>
```

* Avoid vague alt text such as "image", "photo", or the filename.
* Keep it concise; screen readers read it inline with the rest of the content.

### When an image is decorative

If an image adds visual flavour but no essential meaning, use an **empty alt attribute** so screen readers skip it.

```html
<img src="divider.svg" alt="" aria-hidden="true" />
```

* Never leave out the `alt` attribute completely; `<img>` without alt is announced as "unlabeled graphic".

### Icons inside buttons or links

If the text already communicates the action, the icon is decorative.

```html
<button type="button">
  <img src="icon-save.svg" alt="" aria-hidden="true" />
  Save
</button>
```

If the icon is the **only** content, give it a meaningful label:

```html
<button type="button" aria-label="Open settings">
  <img src="icon-settings.svg" alt="" />
</button>
```

### Complex images & visual content (charts, diagrams, prototypes)

Some visuals contain more than a simple picture: they convey data, relationships or meaning.\
Avoid placing essential explanations *only* in tooltips, background images, or hover states, as they are not consistently accessible. Use a **short `alt`** (if applicable) plus a **longer explanation**, ideally using `<figure>` and `<figcaption>`.

* `alt` → concise summary
* `figcaption` → extended description, context or insights
* **Do not repeat** the same text in both.
  * If the image needs *no* extended explanation, use only `alt`.
  * If the caption already *fully* describes the visual, provide an empty alt so screen readers don't read everything twice.

#### Using `<figure>` with images

```html
<figure>
  <img
    src="sales-chart.png"
    alt="Bar chart showing a 20% increase this quarter"
  />
  <figcaption>
    Revenue grew steadily from January to March, mainly driven by the Pro plan.
  </figcaption>
</figure>
```

Screen readers treat the `figcaption` as the **semantic description** of the figure, but **only the non-duplicate parts** should appear there, not a copy of the `alt`.

#### `<figure>` is not limited to `<img>`

You can use it with **any standalone visual**: `<canvas>`, `<svg>`, `<video>`, or a rendered component.\
In these cases, there is **no `alt` attribute**, so the description must be provided entirely in the caption.

```html
<figure>
  <canvas id="revenueChart" aria-hidden="true"></canvas>
  <figcaption>
    Revenue doubled in Q4, with the strongest growth in December.
  </figcaption>
</figure>
```

* `aria-hidden="true"` ensures the canvas (which exposes no semantic info) is skipped by assistive tech.
* The **caption becomes the only accessible description**, which is what users need.
* If the design doesn't require a visible caption (e.g., the insight is already in the UI), hide it visually using the `.sr-only` utility class (see [Labels & Forms](#LabelsForms) for implementation details). This keeps the content accessible while preserving the intended layout.

### Background images

Background images (CSS) **cannot** have `alt`. Ensure the essential content is in HTML, not CSS.

```css
/* Good: purely decorative */
.hero {
  background-image: url("pattern.svg");
}

/* Avoid: meaningful content in CSS background */
.hero {
  background-image: url("fancy-tagline.png");
}
```

### Do not encode text inside images

Text embedded inside an image is invisible to screen readers, translators, and search engines.

* Prefer real HTML text on top of a background.
* If unavoidable, duplicate the text as `alt` or nearby content.

### Emojis and accessibility

Screen readers read emoji names, which can be awkward or confusing. If an emoji conveys important meaning, use `aria-label` to control what gets announced:

```html
<span role="img" aria-label="Warning">⚠️</span>
```

For decorative emojis, you can use `aria-hidden="true"` to hide them from screen readers, similar to decorative images.

## ARIA <a href="#aria" id="aria"></a>

**ARIA** (Accessible Rich Internet Applications) provides attributes that help assistive technologies understand the structure, state and behavior of custom UI components. It is **not** a replacement for semantic HTML; it does **not** fix inaccessible markup, and it does not add keyboard behavior automatically. Moreover, it often adds complexity and can break accessibility if misused. Use it sparingly, and only when native elements cannot express the needed behavior.

### General rule

> **"Use native HTML whenever possible.**\
> **If you can use a `<button>`, `<a>`, `<label>`, `<fieldset>`, `<dialog>`, don't recreate them with `div`s and ARIA."**

### Appropriate ARIA use

Use ARIA only to **add** missing semantics to custom components:

* `role="dialog"` for custom modals
* `aria-expanded` for disclosure widgets
* `aria-controls` to indicate what element a control affects
* `role="alert"` or `role="status"` to announce dynamic messages
* `aria-live` regions for dynamic content
* `aria-current="page"` for navigation
* `aria-selected` for tabs, listboxes, custom selects

```html
<button aria-expanded="false" aria-controls="filters">Filters</button>

<div id="filters" hidden>...</div>
```

### When not to use ARIA

Avoid adding ARIA roles that duplicate or override native behaviors:

* Do not use `role="button"` on a `<button>`
* Do not use `role="link"` on an `<a>`
* Do not use `role="heading"` instead of using `<h1>…<h6>`
* Do not add interactive roles to `div` or `span` elements unless you fully implement keyboard interaction, focus handling and states.

ARIA attributes alone do not provide keyboard support or focus behavior. If you add `role="button"` to a `div`, you also need to handle:

* `tabindex="0"`
* `onKeyDown` for Space and Enter
* focus styling
* preventing unexpected behavior

Which is why native elements are almost always better.

### Attributes that require caution

Use these only when you have a specific reason:

* `aria-hidden="true"` hides content from assistive tech. Only use when elements are truly decorative or duplicated visually.
* `aria-label` can provide an accessible name for icon-only buttons, but prefer visible text whenever possible.
* `aria-labelledby` is often better than `aria-label` because it references existing visible text.

```html
<button aria-label="Open settings">
  <svg aria-hidden="true">…</svg>
</button>
```

### ARIA is not a substitute for correct structure

Avoid using ARIA to compensate for:

* missing labels
* incorrect heading hierarchy
* broken tab order
* inaccessible custom components
* poor contrast or unreadable text
* missing focus management
* images without alt text

All of these should be fixed with HTML, CSS and proper component design.

### Quick mental model

* **First:** Use the correct native element
* **Then:** Fix semantics with structure (labels, headings, lists)
* **Finally:** Add ARIA only where necessary to fill a real gap

For more detailed guidance, refer to **WAI-ARIA (W3C Web Accessibility Initiative)**:\
<https://www.w3.org/WAI/standards-guidelines/aria/>

## Custom UI Components <a href="#customuicomponents" id="customuicomponents"></a>

Custom UI components need to offer the same accessibility guarantees as their native HTML equivalents.\
Buttons, links, checkboxes, dialogs and selects already come with built-in semantics, keyboard behavior, and assistive-technology support. When we replace them with `div`-based widgets, we must recreate all of that manually.

Whenever possible, prefer native elements or accessibility-focused headless libraries like [React Aria](https://react-spectrum.adobe.com/react-aria/), [Radix UI](https://www.radix-ui.com/) or [Headless UI](https://headlessui.com/). When selecting any UI library, make sure its components have documented accessibility patterns and ARIA support.

Use a custom component only when the native option truly doesn't meet the product requirements.

### What every custom component must support

If a component is custom, it must:

* Be reachable via keyboard (`Tab`, `Shift + Tab`).
* Support expected interaction keys for its pattern (e.g. `Enter`/`Space` to activate, arrows to navigate lists, `Esc` to close).
* Expose a clear accessible name (using text, `aria-label`, or `aria-labelledby`).
* Announce its role and state (`aria-expanded`, `aria-selected`, `aria-checked`, etc.).
* Manage focus consistently (no unexpected jumps, no "lost" focus).
* Provide visible focus styles.
* Respect user settings such as reduced motion and high-contrast modes.
* Work reliably with assistive technologies.

If any of these are missing, the component isn't complete.

### Follow established patterns

For complex widgets (selects, tabs, dialogs, disclosures, listboxes, comboboxes), use the official patterns documented in the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)

These patterns define how the component should behave (roles, expected keyboard interactions, states, relationships).\
They're not optional: assistive technologies rely on them.

### Common mistakes

* Using a `div` as a button without adding semantics or keyboard support.
* Creating dropdowns that don't announce whether they're open or that can't be opened with the keyboard.
* Closing dropdowns on click but not on Escape.
* Building "fake inputs" that don't expose a real value to assistive tech or autocomplete.
* Tabs without `role="tablist"` and correct arrow-key navigation.
* Modals that don't trap focus or don't return focus when closed.
* Carousels or sliders that auto-rotate without user control or reduced-motion support.

### Example (simplified)

**Incorrect**

```html
<div class="dropdown" onclick="toggle()">Selected option</div>
<div class="menu hidden">
  <div class="item">One</div>
  <div class="item">Two</div>
  <div class="item">Three</div>
</div>
```

Issues: no semantics, no states, no keyboard behavior, no accessible name.

**Correct (simplified structure + minimal keyboard behavior)**

```html
<button
  id="trigger"
  aria-expanded="false"
  aria-haspopup="listbox"
  aria-controls="options-list"
>
  Selected option
</button>

<ul id="options-list" role="listbox" hidden>
  <li role="option" tabindex="-1">One</li>
  <li role="option" tabindex="-1">Two</li>
  <li role="option" tabindex="-1">Three</li>
</ul>
```

```js
const trigger = document.getElementById("trigger");
const list = document.getElementById("options-list");
const options = Array.from(list.querySelectorAll("[role=option]"));
let currentIndex = 0;

// Make an option focusable by setting tabindex to 0, others to -1
function setActiveOption(index) {
  options.forEach((option, i) => {
    option.setAttribute("tabindex", i === index ? "0" : "-1");
  });
  options[index].focus();
}

// Open the listbox and focus the first option
function openList() {
  list.hidden = false;
  trigger.setAttribute("aria-expanded", "true");
  setActiveOption(currentIndex);
}

// Close the listbox and return focus to the trigger
function closeList() {
  list.hidden = true;
  trigger.setAttribute("aria-expanded", "false");
  trigger.focus();
}

// Open listbox with trigger button
trigger.addEventListener("keydown", (e) => {
  if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
    e.preventDefault();
    openList();
  }
});

// Navigate and select options in the listbox
list.addEventListener("keydown", (e) => {
  if (e.key === "Escape") return closeList();
  if (e.key === "ArrowDown") currentIndex = (currentIndex + 1) % options.length;
  if (e.key === "ArrowUp")
    currentIndex = (currentIndex - 1 + options.length) % options.length;
  if (e.key === "Enter" || e.key === " ") {
    // Do something with options[currentIndex]
    closeList();
  }
  setActiveOption(currentIndex);
});
```

This is still simplified, but shows the minimum expected behavior: keyboard support, state management, and predictable focus handling.

## Reduced Motion <a href="#reducedmotion" id="reducedmotion"></a>

Some users experience motion sensitivity, vertigo or cognitive overload when exposed to large, fast or unexpected animations.\
CSS gives us tools to respect user preferences automatically, and we should use them whenever we animate UI elements.

### Respect `prefers-reduced-motion`

Always wrap non-essential animations in a media query that checks for reduced-motion preferences:

```css
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
```

This should not remove *all* transitions everywhere by default, but it shows the pattern.\
Apply it selectively to components with significant movement.

### Avoid motion-heavy patterns

* Big parallax effects
* Auto-scrolling or scroll-jacking
* Continuous background animations
* Large zooms, bounces or slides
* Animations that move content unexpectedly

Prefer small opacity or color transitions that feel stable.

### Provide stable alternatives

If your component uses motion to communicate state, ensure the same information is available without animation:

* A menu should not rely solely on slide-in movement to indicate it opened.
* A snackbar should not require motion to be noticed. Also use `role="status"` or clear styling.
* A tooltip should not animate from far away; keep movement minimal.

### Animation libraries

If you use animation libraries ([Motion (formerly Framer Motion)](https://motion.dev/), [GSAP](https://gsap.com/), [React Spring](https://react-spring.dev/)):

* Check if they provide reduced-motion helpers
* Prefer opacity/fade transitions over positional movement
* Avoid timeline-driven continuous loops unless essential

Motion example:

```jsx
import { motion, useReducedMotion } from "motion/react";

function FadeIn({ children }) {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: shouldReduceMotion ? 0 : 0.3 }}
    >
      {children}
    </motion.div>
  );
}
```

### When motion is essential

If motion is part of the interaction (carousel, slider, drag & drop):

* Make movement short, predictable and slow
* Allow pause/stop if it auto-advances
* Provide visible focus outlines for keyboard users

Respecting reduced-motion settings is not only an accessibility requirement, it also makes interfaces feel calmer, more stable and more professional.

## Basic Accessibility Testing <a href="#basicaccessibilitytesting" id="basicaccessibilitytesting"></a>

Accessibility doesn't require a full audit to catch the biggest issues.\
A few quick checks during development can prevent most blockers before they ship.

### Keyboard testing (the fastest and most important check)

Try navigating your page with only:

* **Tab** → move forward
* **Shift+Tab** → move backward
* **Enter / Space** → activate
* **Esc** → close modals or menus
* **Arrow keys** → navigate lists, tabs, menus if applicable

If you get stuck, lose focus, or can't activate something, it's inaccessible.

### Screen reader smoke test

You don't need to be an expert. Just test the basics:

* Turn on **VoiceOver** (macOS: Cmd+F5) or **NVDA** (Windows).
* Navigate headings (VoiceOver: VO+Cmd+H, NVDA: H).
* Navigate links (VoiceOver: VO+Cmd+L, NVDA: K).
* Navigate form controls (VoiceOver: VO+Cmd+J, NVDA: F).
* Open your UI menus and dialogs.

Check that elements:

* Have a meaningful accessible name
* Are announced with the correct role
* Have states ("expanded", "selected", etc.)

A short 3-minute test can reveal missing labels, broken structure or incorrect roles.

### Built-in browser tools

Use Chrome DevTools → **Accessibility** pane:

* Check the **Accessibility Tree** (is the element exposed correctly?)
* Look for missing labels or incorrect roles
* Verify contrast directly in DevTools
* Inspect focus order with "Tab" focus highlighting

### Automated tools (first pass)

Automated tools won't catch everything, but they're excellent for fast feedback:

* **Lighthouse Accessibility** (Chrome DevTools → Lighthouse tab → Accessibility audit)
* [**eslint-plugin-jsx-a11y**](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) (during development)

Run these early; treat errors as code smells.

### Test with reduced motion & zoom

* Enable **Reduce Motion** in OS settings
* Ensure the UI still works and doesn't flicker or jump
* Zoom to **200%** in the browser
* Check that layout still holds and nothing becomes unreachable

### When working on components

Test your component in isolation:

1. Can I reach it with keyboard?
2. Does focus go where I expect when it opens/closes?
3. Does it expose the right role and state?
4. Does it still work with reduced motion?
5. Does it behave consistently across devices?

These checks take seconds and prevent major downstream issues.

### When QA time is limited

At minimum, test:

* Keyboard navigation
* Labels on forms
* Focus visibility
* Alt text on images
* Color contrast
* Modals opening/closing correctly
* Error messages being announced

Small habits → huge accessibility wins for the whole product.

## Engineering Accessibility Checklist <a href="#engineeringaccessibilitychecklist" id="engineeringaccessibilitychecklist"></a>

A fast, practical checklist to review implementations before shipping.

* **Use semantic HTML first** (`button`, `nav`, `header`, `form`, `fieldset`).
* **Every input has a label** (visible or visually hidden in short forms).
* **Correct form semantics**: required fields, autocomplete, error messages.
* **Keyboard navigation works** everywhere (Tab, Enter, Space, Esc).
* **No `outline: none`**, focus is always visible.
* **Focus management**: modals trap focus, restore on close.
* **Meaningful alt text**; decorative images use `alt=""`.
* **ARIA is used only when needed** and according to authoring practices.
* **Custom components** support roles, states, keyboard, and focus.
* **Respect prefers-reduced-motion** for animations and transitions.
* **Run automated checks** (Lighthouse, eslint-plugin-jsx-a11y).
* **Test with a screen reader** (basic navigation).
* **Test at 200% zoom** to ensure layout resilience.

### Before shipping

* Can I navigate the UI without a mouse?
* Can a screen reader user understand structure and purpose?
* Are errors and confirmations announced properly?
* Are forms usable and predictable?
* Are animations optional and non-invasive?
* Does the app behave correctly in high zoom or reduced motion mode?

If most answers are "yes", the UI is in good shape.


# Resources

A curated list of reliable, practical resources to go deeper into accessibility.\
These are the references we recommend for both design and development work.

## Official Standards & Reference Material

* [**WAI-ARIA Authoring Practices (APG)**](https://www.w3.org/WAI/ARIA/apg/) - Canonical reference on how interactive components should behave: expected roles, keyboard interaction, states and markup.
* [**WCAG Guidelines (Beginner-Friendly Overview)**](https://www.w3.org/WAI/standards-guidelines/wcag/) - High-level accessibility standards. You don't need to memorise them, but useful to understand the principles.
* [**MDN Accessibility**](https://developer.mozilla.org/en-US/docs/Web/Accessibility) - Clear explanations of semantics, focus management, roles, and browser behavior.

## UI Libraries with Strong Accessibility

When choosing a UI library, prefer options that include correct keyboard behavior, ARIA patterns and predictable focus management out of the box.

* [**Radix UI**](https://www.radix-ui.com/) – Headless, unstyled primitives with excellent accessibility
* [**React Aria (Adobe)**](https://react-spectrum.adobe.com/react-aria/) – Hooks implementing correct ARIA and keyboard behavior
* [**Headless UI**](https://headlessui.com/) – Accessible primitives for React/Vue
* [**Chakra UI**](https://chakra-ui.com/) – Component library with accessible defaults

## Accessibility Utility Libraries

Lightweight libraries to help implement specific accessibility patterns when building custom components.

* [**focus-trap**](https://github.com/focus-trap/focus-trap) / [**focus-trap-react**](https://github.com/focus-trap/focus-trap-react) – Trap focus inside modals, dropdowns, or any container. Essential for accessible dialogs.
* [**tabbable**](https://github.com/focus-trap/tabbable) – Find all focusable (tabbable) elements within a container. Useful for focus management and keyboard navigation.
* [**react-focus-lock**](https://github.com/theKashey/react-focus-lock) – React component that locks focus within its children. Simpler alternative to focus-trap for React apps.
* [**react-aria-live**](https://github.com/AlmeroSteyn/react-aria-live) – React hooks for managing live regions (`aria-live`) to announce dynamic content changes to screen readers.

## Pattern Libraries & Examples

Useful when building a component from scratch or validating a third-party design.

* [**Inclusive Components (Heydon Pickering)**](https://inclusive-components.design/) – Practical, detailed breakdowns of real UI patterns.
* [**A11y Project Patterns**](https://www.a11yproject.com/patterns/) – Curated and approachable examples.
* [**GOV.UK Design System**](https://design-system.service.gov.uk/components/) – Very robust, production-tested components.
* [**Carbon Design System (IBM)**](https://carbondesignsystem.com/components/overview/components/) – Includes accessibility notes per component.

## Testing Tools

* [**WAVE**](https://wave.webaim.org/) (Web Accessibility Evaluation Tool) – Browser extension for quick accessibility checks.
* [**Accessibility Insights**](https://accessibilityinsights.io/) – Free tool from Microsoft for testing web and Windows apps.
* **Lighthouse Accessibility Audits** (Chrome DevTools) – Built-in accessibility scoring and quick wins.
* **NVDA / VoiceOver / TalkBack** – Screen readers you can use to test critical flows.
* **Color Contrast Checkers**\
  [WebAIM](https://webaim.org/resources/contrastchecker/) / [Contrast Ratio](https://contrast-ratio.org/) / Chrome DevTools: "Accessibility → Contrast"

## Design Tools

Tools to help designers check accessibility during the design phase.

* [**Stark**](https://www.getstark.co/) – Plugin for Figma, Sketch, and Adobe XD to check contrast, simulate color blindness, and test touch targets.
* [**Contrast**](https://usecontrast.com/) – Mac app and Figma plugin for checking color contrast ratios.
* [**Color Oracle**](https://colororacle.org/) – Free color blindness simulator for Windows, Mac and Linux.
* [**Accessible Palette**](https://accessiblepalette.com/) – Create color systems with consistent lightness and contrast using CIELAB/LCh instead of HSL. Generates color ranges that meet WCAG contrast requirements automatically, solving the problem of inconsistent perceived lightness in HSL-based color systems.

## General Learning

* [**The A11y Project**](https://www.a11yproject.com/) – Beginner-friendly accessibility tutorials and articles.
* [**WebAIM**](https://webaim.org/) – Deep explanations on contrast, forms, ARIA, screen readers, etc.
* [**Smashing Magazine – Accessibility Articles**](https://www.smashingmagazine.com/category/accessibility/)
* [**CSS-Tricks – Accessibility Articles**](https://css-tricks.com/tag/accessibility/)


