# Echobind > Echobind is a product studio that designs and builds beautiful, reliable web and mobile applications — with deep expertise in healthcare, Stripe payments, AI integration, and React Native. We partner with teams from strategy through design, engineering, and launch. Markdown versions of most content pages are available by appending `.md` to the URL (for example, `/post/` is also served as `/post/.md`). A full-text export of blog posts, case studies, and services is available at https://echobind.com/llms-full.txt. === # Services # AI you can put your name on. Connie, live inside Upstream's FreeBC, answers contraception questions for real patients: clinician-reviewed corpus, no open web, and a decline when it cannot ground an answer. 11,400 users in its first three months. Everything we ship is held to that bar, and if your surface is wrong for AI, we say so before you spend on it. ## How we build ### 01 Close the corpus first If the model can reach material nobody reviewed, you do not have a product. You have a demo with a longer context window. We treat ingestion as the architecture decision. Prompting comes after. ### 02 Decline is a feature A confidently wrong answer about a dose, a denial, or a diagnosis is not a UX miss. It is an outcome. We spend review cycles on the requests where the system is supposed to hold a line, and we ship the decline on purpose. ### 03 Outputs, not specs Human-in-the-loop is a calendar commitment. We put real generated answers in front of the people who own the risk, repeatedly, until the declines and the answers both hold. ## What we build Six capabilities, each anchored to real work. ### 01 Constrained assistants on a closed corpus An assistant that answers from material you have reviewed and declines everything else. Connie, the assistant inside Upstream's FreeBC contraceptive-care platform, is the reference: a clinician-reviewed corpus, no open web, and a decline when it cannot ground an answer. ### 02 Voice agents Registration, scheduling, and intake by voice. We helped Teladoc Health put telehealth visits on Amazon Alexa-enabled devices, and built HIPAA-compliant patient-intake voice work for a pharma services company. ### 03 Computer vision Models that watch video and pull out what matters. Ballers turns a phone recording of a basketball game into highlights, stats, clips, and player profiles, automatically. ### 04 Document intelligence at scale OCR, ingestion, and categorization for documents that arrive by the thousand. ReceiptVault pulls receipts out of email and off paper, reads them, and files them where a search will find them. ### 05 Automation a human still signs We automate the work that burns hours and keep a person on the decisions that matter. For CNY Fertility, automation took per-patient administrative time from 40 minutes to 5. ### 06 Agent infrastructure and deployment Agents your org can actually run: provisioning, memory, allowlists, and no public network surface. Our open-source rome-on-rails template deploys Hermes agents on Railway, each one a private node on a Tailnet. [Read how we run them](https://echobind.com/post/manage-ai-slack-agents) · [rome-on-rails on GitHub](https://github.com/echobind/rome-on-rails) ## The work, checked Every number below comes from a published case study or the product itself. Where we cannot name a client, we say so. ### 01 An assistant that is allowed to talk to a patient. _Constrained assistant_ Connie answers contraception questions inside FreeBC, Upstream's digital contraceptive-care platform. The corpus is closed: authored and reviewed by clinicians, no open web. We put Connie's answers in front of Upstream's clinical team, repeatedly, until the guardrails held. Five months from kickoff to production, with two engineers and a PM. **Client:** Upstream (FreeBC) **The constraint:** Connie is constrained to a clinician-reviewed knowledge base. When accuracy cannot be guaranteed, it declines instead of guessing. **What changed:** 11,400 users in the first three months. Users who completed the quiz were seven times more likely to complete a care pathway. [Read the FreeBC case study](https://echobind.com/work/expanding-contraceptive-care-access-ai-assistant-telehealth-platform) ### 02 Automation for a clinic that could not stop moving. _A human still signs_ NOVO is a patient onboarding and self-service scheduling platform for CNY Fertility, a fertility clinic with eight US locations. Patients book, reschedule, and cancel with no human intervention. Robotic process automation moves the data into the clinic's EMR. Built and launched in eight weeks. **Client:** CNY Fertility **The constraint:** The legacy EMR had no API. We automated data entry into it with RPA instead of waiting for API development. **What changed:** 87% less clinical workload per patient: 40 minutes down to 5. Consult waits fell from over a month to two days. $10K+ saved monthly. [Read the CNY Fertility case study](https://echobind.com/work/scaling-fertility-care-self-service-scheduling-emr-automation) ### 03 Telehealth visits over a smart speaker. _Voice_ We helped Teladoc Health build voice telehealth visits on Amazon Alexa-enabled devices: register, schedule, and connect to a board-certified physician by voice, where the old path was a mobile app or a 1-800 number. The call-center reps stayed in the loop, down to the whisper they hear before a call connects. **Client:** Teladoc Health **The constraint:** The brief was minimal disruption. Every change was scoped to what the voice use case required, and the call-center reps were kept essential rather than designed out. **What changed:** Went live with no significant glitches, in a launch announced publicly by both Teladoc Health and Amazon. [Read the Teladoc case study](https://echobind.com/work/teladoc-building-the-future-of-healthcare-with-voice-assist) ### 04 A chatbot brain the clinicians could retune. _Clinical conversation_ Pear Therapeutics, the company behind the first smartphone app to receive FDA authorization for treating medical conditions, brought us in on Pear 006: a CBT app for depression in Multiple Sclerosis patients. We built a contained chatbot brain that directed patients through guided conversational threads, each one administered by Pear's mental-health experts through a spreadsheet interface. The brain was designed to be replaceable. **Client:** Pear Therapeutics **The constraint:** We evaluated Microsoft Bot Framework, Rasa, and DialogFlow against Pear's clinical constraints, and recommended a contained, expert-controlled brain instead. **What changed:** Pear's clinical team could refine patient interactions without engineers in the loop. [Read the Pear case study](https://echobind.com/work/an-evidence-based-app-to-help-ms-patients-with-depression) ### 05 Set a phone courtside and the game films itself. _Computer vision_ Ballers turns a phone recording of a basketball game into AI-generated highlights, stats, clips, and player profiles: set the phone up courtside, hit record, and go play. Highlights and stats are ready before you leave the gym. The full technical write-up is in progress; the product is public. **Client:** Ballers [See Ballers](https://ballers.gg) ### 06 Every receipt, read and filed. _Document intelligence_ ReceiptVault pulls receipts out of email automatically, reads paper receipts with OCR, and categorizes all of it: search, folders, return windows, spending analytics, exports. The pipeline runs on email integration and receipt-scanning models, not on a person sorting an inbox. **Client:** ReceiptVault **What changed:** Per receiptvault.com: an emailed receipt lands in a user's vault in 0.8 seconds, and pulling one back up takes 2.4. [See ReceiptVault](https://www.receiptvault.com) ### 07 A personal AI agent for every teammate. No public URL. _Agent infrastructure_ Every Echobind teammate gets a Slack agent with its own bot identity, memory, and allowlist. Each deployment is a Tailscale node: SSH for maintenance, outbound Socket Mode for Slack, no inbound ports. We open-sourced rome-on-rails, the Railway template that deploys them, wrapping the Hermes agent by Nous Research. **Client:** Echobind, open source **The constraint:** Private by default: no public URL, no inbound ports, and an explicit Slack allowlist. If the allowlist is unset, the bot refuses every message. **What changed:** A personal agent for every Echobind teammate, and an open-source template anyone can deploy. [Read how we run them](https://echobind.com/post/manage-ai-slack-agents) · [rome-on-rails on GitHub](https://github.com/echobind/rome-on-rails) ### 08 A voice agent that collects patient information. _Voice, under HIPAA_ For a pharma services company, we built a voice agent that collects patient information in a HIPAA-compliant way. We do not have permission to name the client, so we do not. Where we cannot name a client on this page, we say so. **Client:** A pharma services company ## The Surface Review Two weeks. A senior engineer and a product partner. We walk the one surface that would hurt if it were wrong. You leave with one of two things: - A constrained spike you can put in front of the person who owns the risk - A written no, and the cheaper thing that actually solves it If the right answer is do not build this, that is the deliverable. ## You already know which surface this is. [Walk us through it](https://echobind.com/contact) --- [View on echobind.com](https://echobind.com/services/ai) --- # Design Concepts > Brainstorm and visualize multiple visual identities before committing to a given direction. ## Design Concepts Design concepts serve as a foundational step in establishing the visual language and direction for your product. This exploratory phase allows us to visualize multiple visual identities, ensuring that the final design not only resonates with your brand but also appeals to your target audience. Through design concepts, Echobind collaborates with you to explore various design directions, enabling a creative and strategic approach to crafting a product's visual identity. #### What to Expect Defining your product’s design concept is collaborative and iterative, focusing on finding the right visual language that aligns with your project goals. We start by meeting with you to understand your brand and what your product’s visual language needs to communicate. We talk through any constraints that might be relevant for making visual decisions. For example, is your app data heavy? We will want to consider typefaces that are highly legible inside of tables at small sizes. Once we’ve gathered brand and technical information from you, our designers will explore various visual identities by creating style tiles. We’ll then meet with you to review them, refine the ones you like, and ultimately settle on a single direction that will inform the UI Design and Style Guide. #### How It Works **Milestones** Our design concept process often includes the following: - Kickoff Meeting: We start by understanding your vision, brand values, and user demographics to guide the design concept phase. - Design Concept Development - Review and Iteration: ****Together, we review these concepts, gather feedback, and refine our approach until we find the perfect visual direction. **Deliverables** - Mood board (typically a Figma file) - Style tiles (typically a Figma file) - Recordings of any meetings --- [View on echobind.com](https://echobind.com/services/design-concepts) --- # Product Design > Make users genuinely happy with insight-driven design that works. ## How design works at Echobind From navigation to visual design we put the user at the center of the design process. And we take you, the client, on the creative journey from first concept to interactive crowd-pleaser. #### User insights at the heart of everything Awesome products don’t happen by accident. Here are the ways we make sure your user loves you: - an audit of comparable products so we know what you need to beat. - a search of pattern libraries to find data-backed designs that have worked in the past. - interviews with target users to understand their likes and habits. - good old-fashioned experience from doing this a thousand times before. #### Creating the initial concept Our UX designers will bring you a draft user flow, with screens mapped against the journey so you can begin to imagine the experience. Wireframes will show how information, buttons and menus will be organized on the screens. Hardware will be spec’d at this point, if required, for something like a kiosk. #### Testing with users, early and often We’ll run the draft journey by your target user for feedback. And they’ll have some. It’s important to test and adapt now, before you invest in full design. We’ll iterate and test until we get the right balance of user preference and practical business requirements. #### Making your product beautiful With the core journey in place, visual designers will turn those wireframes into a beautiful brand experience. And we’ll future-proof you by using a modular design system that can scale easily and affordably as your product evolves. #### Rolling out a prototype As a final pre-build step, you (and your users) will get to play with a mock-up. You’ll be able to navigate through the prototype to get a sense of the live experience. This is a great opportunity to make any final tweaks before your product goes into build. --- [View on echobind.com](https://echobind.com/services/product-design) --- # Prototypes > Create something that feels like the real deal before investing time and money into development. ## Prototypes Prototypes bring your product's vision closer to reality, serving as an interactive model that simulates the user experience and design of your app before development begins. At Echobind, we specialize in crafting prototypes that effectively bridge the gap between initial design concepts and the final product. By linking all the necessary screens together in Figma, we create a dynamic, clickable prototype that not only showcases what the real application might feel like but also provides an invaluable tool for user testing and clarifying interactive elements for the engineering team. Prototyping is an essential step in our design process, allowing us to iterate on designs based on feedback, ensuring that we're building a product that meets user needs and exceeds expectations. #### What to Expect When we start prototyping, we'll kick things off by discussing the purpose of the prototype and the use cases we need to address. This will determine fidelity and which flows we focus on. Usability testing scenarios usually drive the need for particular functionality in the prototype, but there might also be specific interactions you want to see in higher-fidelity or complicated flows that are best communicated with a clickable prototype. Once we've got your nod of approval on the use cases, we dive into the creation of your prototype using Figma. This stage is about linking those designs together to simulate the actual user experience and functionality of your app, offering you a tangible glimpse into what the final product will feel like. Once we’ve created the first version of the prototype, we’ll send you a link for review, inviting you to explore and critique it. This iterative process allows us to refine and perfect the prototype, ensuring it meets your expectations and addresses the needs of your users. To further validate our design, we typically conduct usability testing. These sessions provide us with invaluable insights directly from your target audience, helping to fine-tune the prototype. Finally, once the prototype has been polished and approved, we prepare for a seamless transition to the development phase. We provide the engineering team with detailed documentation and the prototype itself, ensuring they have a comprehensive understanding of the product's design and functionality. This meticulous approach not only streamlines the development process but also ensures that the final product is a true reflection of your vision, optimized for user engagement and success. #### How It Works **Milestones** - Kickoff Meeting to Define Use Cases - Prototype Development - Feedback Iteration: refining the prototype based on your feedback and, if applicable, usability testing. - Final Review & Engineering Handoff **Deliverables** - A clickable prototype of all or part of the product, typically in Figma - Recordings of any meetings and usability test sessions --- [View on echobind.com](https://echobind.com/services/prototypes) --- # Style Guides > Document your visual identity with styles and components that you can reference and reuse. ## Style Guides A style guide documents your product’s typography, color palettes, spacing, imagery, iconography, and UI components. It’s the best way to communicate your visual identity and bring consistency, longevity, and speed to the design of your product. Style guides help designers and developers adhere to the ideals of your brand and make it easy to implement your visual language across new screens and devices. #### What to Expect At Echobind, we create style guides after working with you on Design Concepts and some amount of UI Design. Our style guides are created in Figma using their features like text and color styles, variables, components, and variants. The size and scope of the style guide varies by project, but most include a color palette, typography, and some UI components at minimum. ![An example style guide from a previous client’s website redesign.](https://cms.echobind.com/assets/6f1b34d6-6296-41b2-a4ba-a48a616b41b5) *An example style guide from a previous client’s website redesign.* #### How It Works **Milestones** - Style Guide Creation - Final Review **Deliverables** - Complete style guide in Figma --- [View on echobind.com](https://echobind.com/services/style-guides) --- # UI design > Refine the experience of your product by applying a visual identity that delights your customers. ## UI Design At Echobind, we focus on crafting interfaces that are not only visually appealing but also intuitive and efficient for users. Our approach to UI design emphasizes the creation of engaging and accessible experiences that reflect your brand's identity and meet your users' needs. Through a collaborative process, we establish a visual language that resonates with your audience and supports your brand and business goals. #### What to Expect UI Design is what most people think of as the main design phase. It’s the part of our process where we use color, typography, spacing, layout, iconography, and other design fundamentals to make your app look like the real thing. During this phase, our designers will take any previously created user flows, sitemaps, wireframes, and design concepts and use them to create the high-fidelity mockups of your product. This process usually takes place over a number of weeks depending on how many screens we need to design. We will usually design a set of screens for a given user flow, meet with you for review, iterate based on your feedback, and repeat this until the full app is designed. As part of this process, we usually create a style guide that includes the components, colors, and typography that will be used in product. If you already have an established brand, we’ll use it as the starting point and extend and adapt it to the needs of the product. #### How It Works **Milestones** - Identify and Design Necessary Screens - Iterative Reviews: we’ll meet with you regular to review progress and iterate based on your feedback - Final Approval - Documentation for Handoff to Development **Deliverables** - High-fidelity visual design with realistic copy, created in Figma - Recordings and notes from any meetings --- [View on echobind.com](https://echobind.com/services/ui-design) --- # Usability Testing > Get your prototype in front of users with realistic scenarios to see where they get stuck. ## Usability Testing In the realm of digital product development, understanding how users interact with your product is not just beneficial—it's essential. We offer usability testing to assess and pinpoint how real users navigate and experience your app or website. This process not only identifies usability issues but also provides valuable insights into user behavior, preferences, and satisfaction levels. At Echobind, we take a systematic approach aimed at validating (or invalidating) design assumptions, identifying potential improvements, and ensuring that your product aligns with user expectations before you invest in engineering. By involving users directly in the testing process, we can uncover usability problems and make adjustments to our designs, saving you the money and time it might cost to correct issues once the product has been developed. #### What to Expect At Echobind, we offer usability testing as a stand alone service for existing products, as part of team augmentation, and as a part of any new app design. We use two different types of usability testing, moderated and unmoderated, depending on the needs of the project. **Moderated Testing** One of our design researchers handles every part of the process manually, from creating the testing script, sourcing and scheduling participants, to running the sessions. While moderated tests can be time-intensive, they offer the benefit of allowing the researcher to ask follow-up questions in real time resulting in richer qualitative insights. **Unmoderated Testing** Participants are recruited using a third party tool, [UserBrain](https://www.userbrain.com/en/), and the sessions are unmonitored. Our researchers write a series of task scenarios and prepare a prototype. UserBrain then recruits participants from their pool of testers and we receive recordings of the sessions once they are complete. We use unmoderated testing the most as it’s the fastest and easiest way to recruit a larger number of testers and get quantitative insights about your product. Once we’ve completed the test sessions, our team will spend time reviewing and analyzing the recordings and notes from each one. We’ll categorize insights into themes, capture quotes, and sometimes pull out short clips from the tests for particular trouble spots so you can watch how participants respond to various interactions and features. Once we’ve completed our analysis, we’ll use the insights to make recommendations about the next steps. If we are testing a prototype we’ve created, we’ll use those insights to make design changes before finalizing things. #### How It Works **Milestones** - Outline Usability Test Objectives - Prepare Task Scenarios & Script - Recruit Participants - Conduct Test Sessions - Analyze Recordings & Create Final Report **Deliverables** - Usability test plan - Script and task scenarios - Final report outlining insights, quantitative data, and our recommendations for next steps - Recordings and notes from any test sessions --- [View on echobind.com](https://echobind.com/services/usability-testing) --- # User Flows > Outline the core screens, features, and functionality for all of your users before considering layout. ## User Flows User flows are a fast and low-cost way to map out the screens and actions a user will take when navigating your app. They’re are a great tool for visualizing everything you need your product to do without any of the distraction of UI elements and without any of the cost of actually coding something. Our team will create user flows before creating higher-fidelity artifacts like wireframes or prototypes so that we can clarify use cases and prioritize features that will add the most value for your business and customers. **Benefits of user flows:** - Iterate and test out multiple interactions before committing to a direction. - Streamline design and development by visualizing every screen and action in one diagram. - Provide a more accurate translation of design into functional features and reduce the chances of misinterpretation during the development phase. - Easily visualize which features will likely take the most resources, making it easier to prioritize the next phase of development. #### What to Expect The goal of creating user flows is to craft a shared vision for your end-to-end customer experience. Typically our team will leverage information from a discovery workshop or kickoff meeting to understand your product use cases and feature requirements. Once we know who your users are and which features we need to design and build, a member of our design team will create the user flows in a Figjam or Whimsical file. Once we’ve created the first version, we’ll typically send you a loom video for review or schedule a meeting to address any questions you might have. We’ll then work with you and our strategy team to prioritize features and create a roadmap for design and development. #### How It Works **Milestones** - Kickoff Meeting: we’ll discuss your product vision and feature requirements - Diagram Review - Iteration & Prioritization: we’ll often refine the user flows and use the final version to prioritize which screens we will design **Deliverables** - User flow diagram (typically a Figjam or Whimsical file) - A loom walkthrough of the flows - Recordings of any meetings or sessions --- [View on echobind.com](https://echobind.com/services/user-flows) --- # User Interviews > Understand your customers and make sure you’re building a solution that meets their needs. ## User Interviews User interviews are one of the research methods we offer to understand the needs and perspectives of the people who will be using your product. They’re one of the most direct ways to understand the motivations, expectations, and pain points of your target audience, and they can provide insights that lead to a better user experience and new ideas for innovation. Talking directly to your users and hearing what they have to say can boost confidence that you’re building the right product, generate ideas for new features, and give you a unique advantage over competitors. **User interviews are a great way to:** - Find out how your users are currently solving the problem your product is trying to solve - Discover new problems or unknown pain points that lead to product innovation - Understand in what ways your product is not meeting customer expectations - Uncover why customers might abandon your product so you can improve customer retention - Identify the exact language your users use so you can reflect their mental model in your product As with all of our services, our focus is always on providing the most value to you on a timeframe and budget that fits your needs. We view user interviews as one of the most efficient ways to gather the information you need to improve your product. #### What to Expect Our user interviews consist of five moderated sessions with individuals from your target audience. We usually start by identifying each unique type of user who will be using your product and recommend conducting 5 interviews with individuals of each type. Before we conduct the interviews, our team will meet with you to discuss the goals for the interviews and the research questions that we need to answer. Once we understand your goals, we’ll work with you to recruit the right participants and schedule all sessions. At Echobind, our user interviews follow a semi-structured process, which means our team will prepare a general script and set of questions that are flexible and allow for nuanced conversation with each participant. A member of our design team will moderate each session. If you’re available, you’re welcome to listen in on these sessions and debrief with our team afterward. If you’re unable to attend, you will have access to the recordings after the interviews are concluded.* Once we’ve finished the interviews, our team will review our notes and transcripts to synthesize themes and patterns. We’ll write all of this up in a final report with recommendations for next steps. *We only record sessions if a participant has given permission #### How It Works **Milestones** Our user interviews often include the following milestones: - Kickoff Meeting: we’ll meet with you to discuss goals and questions, participant screening criteria, and participant incentives - Interview Guide Review - Moderated Interviews & Team Debriefs - Final Report Delivery **Deliverables** After user interviews, we typically send the following: - Interview guide - Recordings & transcripts from all sessions - Final findings & recommendations report - Notes from all meetings - Recordings of all meetings --- [View on echobind.com](https://echobind.com/services/user-interviews) --- # UX Audits > Identify the top usability issues in an existing app and create a plan for how to address them. ## UX Audits Our UX Audits are a comprehensive review of your app or website to assess its usability and overall user experience. UX Audits are a great way to invest in UX because they allow our team to uncover problems that might be hindering your product’s growth without having to create any new design artifacts, conduct research, or run usability tests. #### What to Expect Our UX Audits are typically conducted by one of our designers and can be completed within a week. When we perform a UX Audit, we start by talking with you about your business goals and important use cases. Once we understand what your product needs to do for your business and your customers, we benchmark it against UX best practices and give you a detailed report with findings and recommendations. Our audits typically cover things like visual design, interaction design, layout, hierarchy, navigation, accessibility, and bugs. Our main goal is to give you actionable recommendations that will have an immediate, positive impact on your product. #### How It Works **Milestones** - Initial Meeting: our team will meet with you to understand your business, product, and key use cases. - Review of Findings: once our team has completed the audit, we’ll review the findings with you and answer any questions you might have. **Deliverables** After we’ve finished a UX audit, we typically send the following: - Final findings and recommendations report - Screen annotations (typically a Figma file) - Recordings of any meetings or sessions --- [View on echobind.com](https://echobind.com/services/ux-audits) --- # Wireframes > Iterate quickly, explore foundational UX ideas, and make good decisions about what matters most. ## Wireframes Wireframes are the first step in the design process where we start to visualize full layouts and interactions as a user might experience them. Wireframes are intentionally low-fidelity. They look like rough, black and white blueprints of an app, and their main purpose is to communicate layout, functionality, and the overall user experience of an app without any of the visual design. This intentional low-fidelity allows us to iterate much more quickly on the interaction design without having to worry about pixel-perfection. Wireframes can also be used to test ideas with users or solicit feedback about technical feasibility from the engineering team. #### What to Expect When we create wireframes we use everything we’ve learned from the discovery workshop, user interviews, UX audits, and user flows to design a “rough draft” of the app. We’ll typically start with core features and flows, which we then review with you to refine and iterate. Once we’ve established the navigation patterns and overall structure for the app, we’ll then create wireframes for edge cases and error states. Sometimes we will use wireframes in usability testing to validate ideas before exploring visual identity. Based on feedback from you and your team and any feedback we collect from early usability testing, we’ll either 1) refine the wireframes or 2) move right into UI design and apply the feedback in the high-fidelity designs. For apps that already have an established design system we can usually skip wireframes and skip straight to UI design. #### How It Works **Milestones** - Identify and Create Wireframes for Necessary Screens - Iterative Reviews: we’ll meet with you regular to review progress and iterate based on your feedback - Final Approval **Deliverables** - Low-fidelity wireframes in Figma - Recordings and notes from any meetings --- [View on echobind.com](https://echobind.com/services/wireframes) --- # Workshops > Uncover new insights, explore the problem you’re trying to solve, and generate ideas together. ## Workshops Our design workshops are interactive and collaborative sessions designed to spark conversation, generate ideas, and synthesize what your team knows. We use workshops to set product vision, align your team, and cultivate buy-in. Our goal for each workshop is that everyone leaves with a shared understanding, feeling confident and excited for the project ahead. #### What to Expect Workshops can vary in content and length, but they all involve a series of activities designed to answer questions and extract knowledge about your business and product. A designer from our team will meet with someone from your team to plan the workshop according to the needs of the project. Once we decide on the format and duration, our team will plan the agenda and prepare materials for each activity. We’ll work with you to coordinate meeting times that work for those who need to attend, and then we’ll facilitate the whole thing. Once the workshop has ended, our team will synthesize what we learned and send you a recording of the workshop along with the digital whiteboard and any other relevant artifacts. #### How It Works **Milestones** Our workshops often include the following: - Planning Session: our team will meet with your team lead to discuss the goals of the workshop and plan the content - Demo of Existing Experience (if applicable) - Workshop Session(s): Typically one or more 2 - 4 hour remote sessions **Deliverables** After workshops, we typically send the following: - Digital whiteboard(s) from each session (typically a Figjam file) - Recordings of all sessions - Any notes and synthesis reports --- [View on echobind.com](https://echobind.com/services/workshops) --- # Software Engineering > Create a product that powers on and on, no matter what you throw at it. ## How engineering works at Echobind We go hard on execution. Engineering is hard-baked into the whole ideation and design process so that when it comes time – the build is seamless and the product delivers. #### Focus on getting things done Engineers help brainstorm the ideas and features we suggest for your product, so you know everything is doable before we start. And doable on time. #### Stability is a non-negotiable Users don’t like spinning wheels, so we put a premium on stability – with data that moves and screens that respond in real time, every time. No matter what crazy stuff your users try to do. #### Built to evolve You want a product that can flex and scale as new users, new use cases, and new technologies emerge. The build will leave the door open to add extra features, switch to bigger databases, move to faster platforms, or adopt new billing models. No matter how awesome your product is for today, we want it to be even better tomorrow. #### Thinking of every detail Tell us what you need and, yes, we’ll deliver that – but we’ll also think about the extra stuff that often gets neglected. Like the website where people download your app, for instance. Or the SEO tagging that helps people find that website. It’s about more than responding to briefs. It's about using our experience to deliver complete value. --- [View on echobind.com](https://echobind.com/services/software-engineering) --- # Zero to launched in weeks, not quarters. Strategy, design, and engineering in one senior team from the first day. A patient scheduling platform in eight weeks. A mobile app in both stores in two months. An AI-assisted care platform with 11,400 users in its first quarter. Tell us what you are building and you will have a budget range within two business days. ## One team, start to launch The people in the first workshop are the people who ship it. Strategy, design, and engineering aren't separate teams passing the project along, so what gets decided on day one is still true when the code goes out. ### 01 Decide what version one is A workshop with strategists, designers, engineers, and a PM. Half a day or three, depending on the product. You leave with business requirements, user needs, technical constraints, a budget, and a schedule. Then a competitor audit and target-user interviews show where parity ends and the real opportunity starts. [How we run strategy](https://echobind.com/services/strategy) ### 02 Design it, then test it while changes are cheap User flows first, then wireframes, then visual design, then a working prototype. Each round goes in front of target users before the next one starts. That is what keeps the expensive changes out of the build. [How we design](https://echobind.com/services/product-design) ### 03 Build it with the engineers who were in the room Engineering sits in the workshop and the design reviews, so the build plan exists before the first commit. The product ships doing what you asked, and it is structured to take on the next user type and the next integration without a rewrite. [How we build](https://echobind.com/services/software-engineering) ### 04 Launch it, then read the data Launch is the start of the useful part. Post-launch analytics are in the original budget, so the first month of real behavior turns into the next set of changes instead of a new proposal. ## Shipped from zero Three products that did not exist before. Every number comes from a published case study; timelines are kickoff to launch. ### 01 A clinic platform that books itself. _Healthcare platform_ NOVO is CNY Fertility's patient onboarding and self-service scheduling platform: eight US locations, patients booking, rescheduling, and canceling with no staff in the loop. The legacy EMR had no API, so robotic process automation moves the data in. Ruby on Rails, Stripe, SavvyCal Appointments. **Client:** CNY Fertility **Kickoff to launch:** 8 weeks **What changed:** 87% less admin time per patient, 40 minutes down to 5. Consult waits from over a month to two days. $10K+ saved a month. [Read the CNY Fertility case study](https://echobind.com/work/scaling-fertility-care-self-service-scheduling-emr-automation) ### 02 A care platform with 11,400 users in its first quarter. _AI-assisted care_ FreeBC is Upstream's digital contraceptive-care platform: a method comparison library, a recommendation quiz, telehealth and in-person care routing, an analytics layer, and Connie, an assistant constrained to a clinician-reviewed knowledge base. Two engineers and a PM took it from kickoff to a live Colorado pilot. **Client:** Upstream **Kickoff to launch:** 5 months **What changed:** 11,400 users in the first three months. Quiz completers were seven times more likely to complete a care pathway. [Read the FreeBC case study](https://echobind.com/work/expanding-contraceptive-care-access-ai-assistant-telehealth-platform) ### 03 An e-commerce marketplace for an industry that ran on paper. _Marketplace_ O'Neil Software already ran the back office for record storage centers. Annex turned that network into a consumer-facing marketplace: shop for storage, pick a vendor, sign the service agreement, pay, and manage orders and invoices in one account. Next.js, Directus, tRPC, and Stripe, with shared components built once for web and mobile. **Client:** O'Neil Software **What changed:** The first consumer marketplace in record storage. Typed end-to-end contracts caught issues early enough that production deploys needed zero rollbacks, and the marketing team publishes content without an engineer. [Read the Annex case study](https://echobind.com/work/oneil-software-annex) ## How we start Tell us what you want to build. Within one to two business days you get the main technical and design hurdles in the idea, and a rough budget range for clearing them. No discovery call required to get a number. Then, if we fit: a working session, not a proposal deck. Half a day to three days with the full team, deciding requirements, users, tech, budget, and schedule once. You leave with a plan anchored to your objectives: prioritized features, flagged technical requirements, a budget and a delivery schedule. If money is tight, we tell you what to cut and still hit the goal. If the right answer is a smaller version one, or none yet, that is what you will hear. ## When it's done, it's yours - **The repository, the infrastructure, the store listings.** Yours, not ours. Nothing about the build depends on keeping us around. - **A codebase your own engineers can work in.** Annex's marketing team publishes without an engineer. That is the standard. - **Knowledge transfer before we leave.** And a team that comes back for the audit, the upgrade, or the next version when you want it. ## Tell us what you're building You will hear back within two business days with the hurdles we see and a budget range. [Get in touch](https://echobind.com/contact) --- [View on echobind.com](https://echobind.com/services/new-product) --- # Fix what's broken. Keep what works. Most products that feel broken don't need a rewrite. ezCater's site went from a 43 mobile Lighthouse score to 88. Bose's Home Speaker 500 trial campaign had been in development for 18 months when we arrived; it shipped two weeks ahead of schedule on the codebase they already had. Calibrate's progress screen was redesigned, tested with members, and handed to engineering in about four weeks. Tell us what's not working, and don't hold back. You'll have a plan and a rough budget back within two business days. ## What's not working? Five things we hear most, and what we did about each one for a client whose case study is published. ### 01 The site got slow and nobody knows why We profile it before we touch it. On ezCater's site, the main thread was busy with JavaScript doing work CSS could do, images were missing width and height attributes, and Google Maps and a chat widget loaded before anyone had interacted with the page. Fixing those took the mobile Lighthouse score from 43 to 88. [Read the ezCater case study](https://echobind.com/work/corporate-hospitality-aims-for-faster-food) ### 02 The project has been almost done for over a year Bose's Home Speaker 500 trial campaign had been in development for 18 months. Our audit found UX side effects and security vulnerabilities, and we weighed a port to React. We kept the codebase instead, fixed the high priority bugs, and delivered two weeks ahead of schedule. [Read the Bose case study](https://echobind.com/work/helping-bose-ship) ### 03 Users reach the screen and then don't do the thing A screen can look finished and still fail its one job. Calibrate's progress screen graphed weight in pounds and gave members nothing to measure against their 10% goal. We prototyped a fix, tested it with Calibrate's own members over Zoom, A/B tested a second version, and handed a documented feature set to engineering in about four weeks. [Read the Calibrate case study](https://echobind.com/work/redesigning-a-core-ux-flow-in-calibrates-mobile-app) ### 04 Payments work today and we're not sure they'll survive the launch PlayOn! Sports processes over 300 transactions per second on Friday nights and was moving from Stripe's legacy Charges API to Payment Intents ahead of a launch. Our certified Stripe architects spent two weeks in their front-end and back-end code, checked PCI compliance and peak-load behavior, and reported what was sound and what to change. [Read the PlayOn! Sports case study](https://echobind.com/work/helping-play-on-sports-scale-their-stripe-payments) ### 05 One feature is tangled into everything else Brilliant for Educators lived inside Brilliant's core platform, spread across Vue 2 and Next.js code. We gave it a Next.js application of its own and lifted over the parts it needed; the brief was to do that without disrupting what was already working. Version 2.0 launched on schedule for the new school year. [Read the Brilliant case study](https://echobind.com/work/empowering-educators-one-lesson-at-a-time) ## How we start ### 01 Tell us what's not working All of it, and don't hold back. Within two business days you get a plan for what we'd fix first and a rough budget for doing it. ### 02 An audit by the people who'll do the work Before anything changes, the engineers who will ship the fix read the code. Bose's initial audit discovered UX side effects and security vulnerabilities. PlayOn's took two weeks and covered their Stripe front end, back end, and webhook handlers. You get what's sound, what's broken, and the order to fix it in. ### 03 Fix it, then ship it on the date Sometimes the answer is keep what you have and fix the priority bugs; that's what got Bose to launch two weeks early. Sometimes it's a new application that lifts the working parts out of the old one, as with Brilliant for Educators. Either way there's a date, and the work above is what shipping on it looks like. ### 04 What you have at the end The fix in production and the findings in writing. PlayOn got a final report with actionable recommendations. Calibrate got a finalized prototype and a documented feature set for engineering. And a team that knows the codebase if you want to keep going. ## Tell us what's not working. Slow pages, a screen users abandon, a payments migration you don't trust, a feature stuck inside the monolith. You'll hear back within two business days with a plan and a rough budget. [Tell us what's not working](https://echobind.com/contact) --- [View on echobind.com](https://echobind.com/services/improve-an-existing-product) --- # Engineers who join your team and ship. Axios embedded Echobind engineers for eight months to ship a React Native app on a deadline their in-house team didn't have the capacity to hit. Univision and Teladoc Health have done the same. The engineers and designers you talk to are the ones who join your team. Tell us where the gap is and you'll have a staffing plan and a rough budget within two business days. ## How embedding works 1. **Map the gap.** Your requirements and a map of your in-house coverage: who's on what, what's covered, what isn't. Sometimes the gap is capacity. Sometimes it's a specialty like React Native, voice, or a payments integration. That decides who joins and for how long. 2. **They join your team.** Your repo, your standups, your review process, your PM tooling. Our pull requests go through your review queue like anyone else's until your team decides otherwise. 3. **They ship inside your constraints.** The work fits your architecture and your operations. Changes stay as small as the job allows, and the people your product touches, like a call center, are part of the plan rather than surprised by it. 4. **Your team owns it after.** Handoff is planned from the start, so your engineers can keep working in the codebase without us. Abby Care's own web developers now contribute to the app we built, and they brought us back months later for an audit and an upgrade. You keep a direct line to Echobind the whole time if you need to switch people or ways of working. ## Who joins ### Engineers Web and mobile, front end and API. React and React Native are the daily stack; Stripe, Medplum, and Alexa integrations are on the case-study pages. **Stack:** React, React Native, Next.js, Expo, Stripe ### Designers and UX Product designers who take one screen or one flow from problem to engineering spec. At Calibrate that was a prototype, testing sessions with their members, and a documented feature set for engineering in about four weeks. **Stack:** Figma, Prototypes, Member testing ### Project management support For when the bottleneck is coordination as much as code: ticket triage, build requests, and keeping the queue honest inside your PM tooling. **Stack:** Asana ## Three teams we joined Three engagements inside other teams' repos. Every name and number comes from a published case study. ### Axios: Eight months inside the Axios engineering team. **The gap:** A tight deadline for new iOS and Android apps, and no in-house capacity to build them while supporting the core infrastructure. **What changed:** Embedded engineers built app features, contributed to the news feed API, and set up post-launch monitoring. The apps launched on time and received thousands of four- and five-star ratings. [Read the Axios case study](https://echobind.com/work/delivering-news-in-a-flash) ### Univision: Onboarded fast, then trusted with the whole front end. **The gap:** After acquiring VIX, Univision needed a proof of concept of the redesigned PrendeTV app across iOS, tvOS, Android, Android TV, and Fire TV, and needed whoever joined to onboard quickly. **What changed:** Our engineers sat in the PrendeTV team's daily standups, started with the Video On Demand section in React Native, and were eventually trusted to review and approve the team's own pull requests. The scope grew to the whole front end including live TV. The app shipped on the App Store, Google Play, Fire TV, and Roku, and we still maintain it. [Read the PrendeTV case study](https://echobind.com/work/introducing-20-000-hours-of-content-to-prendetv) ### Teladoc Health: A voice telehealth visit, launched with Amazon. **The gap:** A patient should be able to start a telehealth visit through an Alexa device, without disrupting the call center representatives who handle those visits today. **What changed:** We worked through their stack with engineering, product, and call center teams so every change was limited to what the voice flow required. It went live with no significant glitches, and Teladoc Health and Amazon announced it together. [Read the Teladoc Health case study](https://echobind.com/work/teladoc-building-the-future-of-healthcare-with-voice-assist) ## Two business days to a staffing plan. Tell us the gap, the deadline, and the stack. You get a proposed plan (which roles, for how long, attached to what) and a rough budget. Questions about your stack get answered by the people who will do the work, because they're the ones on the call. ## Tell us where the gap is. Deadline, specialty, or both. [Tell us what you need](https://echobind.com/contact) --- [View on echobind.com](https://echobind.com/services/power-up-your-team) --- # Strategy > Nail the thinking behind your product so it delivers real value for your users, and your organization. ## How strategy gets done at Echobind Let’s get that idea out of your head and into an organized, buttoned-down plan. We’ll work alongside you to identify the priorities and make informed, data-driven decisions. #### Tell us what you’re working on Send us what you’re working on and we’ll help focus your thinking on the most important decision points. If you find us helpful, we can jump into a workshop to flesh things out. [Get in touch](/contact). #### Workshop your priorities with experts Discuss your key goals and deliverables with our strategists, designers, engineers, and project managers. They’ll help distill a set of key priorities and get to work on a plan. ##### Strategy delivery (with all thinking explained) Your strategy will hit the key points we agreed on in the workshop, with the background and rationale for every recommendation. Here’s what you get. **Product strategy, including:** - user research and competitor analysis to inform specifications and design - prioritization of features so you can make smart decisions - engineering recommendations so your product can grow and evolve **Technology choices, including:** - tech recommendations that suit the scale and life-stage of your product - flexible architecture and seamless integrations - advice on AI tools and risk management **Prioritization and budgeting, including:** - prioritization of features to support smart planning - resource recommendations to help max out your budget - end-to-end scheduling for clarity on milestones **Post-launch plan, including:** - post-launch analytics and reporting - data-driven optimization recommendations - no-surprises – with optimization included as a line in the original budget #### Project Management A comprehensive strategy has lots of moving parts. You’ll have complete visibility of progress throughout. And a project manager who’s up-to-date on all workstreams, at all times. --- [View on echobind.com](https://echobind.com/services/strategy) === # Work # Launching a First-of-its-Kind SaaS Business: Clerestory > Echobind built and launched Clerestory, a SaaS company that simplifies invoice creation for kitchen and bath remodeling. --- [View on echobind.com](https://echobind.com/work/clerestory-saas-rebranding-marketing-website-and-tradeshow-materials-support) --- # Introducing 20,000+ Hours Of Content To PrendeTV > Univision, a leader in the Spanish TV marketplace, took on the challenge to re-envision PrendeTV into a more robust AVOD-free media streaming service after acquiring VIX in 2021. The Problem ----------- In order to incorporate the newly acquired content from Vix, the design and engineering teams at Univision worked in tandem to update the PrendeTV app. Univision looked to Echobind to augment within their existing dev team, and assist them in building a Proof of Concept (POC) of their newly redesigned media streaming app with support on iOS, tvOS, Android, AndroidTV, and FireTV platforms. Specifically, our initial directive was to build out the Video On Demand (VOD) section — think Netflix — for Apple and Android mobile devices. POC app completion needed to happen relatively quickly to stay within beta launch deadlines, while also allowing enough time for proper Quality Analysis, so our team needed to onboard quickly and immediately get working on the new design direction of the app. Client's Plan ------------- With a successful launch of PrendeTV earlier this year in March, the integration of VIX content will allow for Univision to offer a robust selection of the most popular Spanish-language programming available to viewers in the U.S., Mexico, Brazil and many other countries. Echobind participated in daily meetings with the Univision team, providing build updates and recommendations, while getting directives on what our team should focus on week to week. Traps and Challenges -------------------- Collaborating with other dev teams often requires us to tactfully work through a few technical challenges. Below are some that our team encountered and overcame: * Building for tvOS and Android TV using React Native * Implementing the SDK for Fire TV testing on actual Fire TV Stick devices * Outdated/version locked packages which were tied to unknown pieces and complexity of tracing the flow of data/props through components. * Prior to getting Code Owner privileges for the repo, PRs would tend to queue up for Univision review as we were introducing new pieces of the build at a fast pace. * Getting feedback, sending builds to testflight/Android's version, and making sure the most up to date branches have been sent out for testing via Applause. We also needed access to SDKs that had components or functionality that we could not change otherwise. * Adding Auth: [iOS has particular standards](https://www.nytimes.com/2021/09/13/technology/apple-software-update-spyware-nso-group.html) if content is meant to be locked behind an account registration and Univision initially wanted a particular auth flow that made the account feature difficult to find. * Making sure the team is equipped with the necessary physical devices to test on, which helps navigate issues on emulator/simulator devices vs physical devices. * Translations: Team assisted in translating Invision designs from Spanish to English and Portuguese. The Fix ------- Echobind helped Univision build out the Video On Demand section of the PrendeTV mobile, TV and web app by transforming static designs into a fully functional React Native UI. Beyond our initial assignment of VOD, the collaboration grew into our team supporting all aspects of the front end build, including VLL (think live Cable TV). The team also performed rigorous QA testing of all versions of the app across different devices, especially when making any bug fixes or introducing additional features. Beyond the engineering scope, we helped their PM with managing tickets in Asana and triaging incoming build requests. Crucial to our joint success was transparent communication on features and bug fixes. Signs of Success ---------------- Our joint efforts led to not only a successful proof of concept, it blossomed into full adoption and was made available in the Apple App Store, Google Play Store, and for TV platforms including Fire TV and Roku. We continue to maintain the app through bug fixes, new feature implementation, and A/B testing through Google Firebase. The app has been downloaded thousands of times, with users spanning from the U.S. through all of Latin America, and even in Portugal. --- [View on echobind.com](https://echobind.com/work/introducing-20-000-hours-of-content-to-prendetv) --- # Streamlining the Billing Process > We built a seamless way for CNY staff to create complex financing plans and then allow patients to agree to finance terms and make a manageable monthly payment using Stripe. CNY Fertility is on a mission to provide the most affordable fertility care in the industry. Echobind built CNY Fertility’s new Fertile Financing Platform for patients and the CNY Fertility staff. What we built was a seamless way for their team to create individualized financing plans. Their patients are able to review and agree to financing terms, review their balances, and make payments in a variety of ways. ![Summary of a patient invoice for CNY fertility.](https://cms.echobind.com/assets/eb06e2c1-5ae3-4a49-acbd-0f780418da3f) The Concept ----------- CNY Fertility’s goal was to remove the manual processes for enrolling patients into payment plans. Their team spent a significant amount time setting up each patient's financing terms. This included applying line item discounts, re-calculating monthly payments and capturing payment information. Automating this manual work would allow team members to spend more time with the patients, improve CNY Fertility's cash flow and give patients more control. And by bringing all payments into a single system, CNY Fertility could better track data on their financing plans and patient enrollment. This presented three interesting challenges for our team. Challenge #1 - Data Migration ----------------------------- Echobind's first challenge was to migrate CNY Fertility's customers from their existing payment processor. We wanted this to be a seamless process. Stripe offers several options to help migrate data, but we wanted CNY Fertility to have more control over the migration process. We created a tool in their billing platform to allow admins to migrate patients to the new system if it fit their migration criteria. This gave CNY Fertility granular control over the migration process. Patients received an email about the migration and could review and accept their new payment plan. Upon acceptance, patients securely store their payment information on Stripe. CNY Fertility staff members then received a notification about the migrated user and cancelled their old payment plans. ![Online form with fields to add a patient's information and payment for services.](https://cms.echobind.com/assets/d8c2b385-d7af-4378-bcc1-613fecd77078) ![Online form summary of costs with options to remove line items.](https://cms.echobind.com/assets/c0a56478-16c1-4006-8304-21edc66fa638) Challenge #2 - Facilitating Financing ------------------------------------- Stripe provides excellent options for Buy Now, Pay Later through Klarna or Affirm integrations; however, because CNY Fertility provides its own financing, those weren’t viable options. Luckily since Stripe’s list of products is so robust, approaching this problem in another way was feasible. To apply the Buy Now, Pay Later concept with CNY’s own financing, the EB team structured Stripe Subscription Schedules so that all outstanding funds are calculated and collected over a number of iterations equal to the terms the patient selected during checkout. For example, suppose a patient chooses to finance their treatment for six months. In that case, a Subscription Schedule is created for six iterations for the total amount of the treatment, divided by the number of iterations. * Build features for the new mobile apps * Contribute to their news feed API * Run tests and troubleshoot complex performance problems * Set up monitoring tools to track and manage post-launch performance ![Online form with a sliding scale showing amount for monthly payment and what is due today.](https://cms.echobind.com/assets/a39eb5a4-687a-4b0a-927a-32091aca0155) Challenge #3 - Managing Payment Plans ------------------------------------- Our mission was to automate much of the payment and finance process. One ask from CNY Fertility was to keep as much of a human touch as possible when interacting with customers. This is a great reminder that software is made for humans. And because life happens, it was critical for us to add tooling that allowed for payment adjustments. CNY Fertility needed the ability to change payment terms at any time for any reason. Echobind built the ability for admins and patients to adjust – or remove – the down payment. When generating quotes, admins can apply line-item discounts. And when patients want to pay off the remaining balance, the portal can apply a discount automatically. Finally, admins can use the portal to add payments made when a patient pays with cash at a clinic. Any cash payment made will reduce future monthly invoices. Everything described here flows through Stripe's Payments, Subscriptions and Invoices products. The Outcome ----------- CNY Admins now have consistency in quoting practices, available services, service prices, discounts, legal terms, and payment options. Creating a quote is as simple as choosing line items, applying discounts, and clicking ‘Send’. The system still gives them the flexibility they need to offer discounts or waive down payments on a case-by-case basis. Patients now have an easy, streamlined way to view quotes, review terms, reject quotes, or accept and enroll in a monthly payment plan. Their billing portal shows them their monthly payment and outstanding balance at a glance, so they never need to wonder about the state of their payment plan. As of this writing, a number of patients have successfully paid for services through the new portal, with some of those opting for a monthly payment plan. As the portal continues to roll out, even more patients will be onboarded and enjoy the convenience that the patient dashboard provides. And CNY is still able to preserve the personal interactions and support they give to their patients through every step of their treatment, which is what makes them stand out in their industry. --- [View on echobind.com](https://echobind.com/work/cny-fertility-stripe-billing-implementation) --- # Teladoc Health: Building the Future of Healthcare with Voice Assist > Teladoc hired Echobind to develop an Amazon Alexa integration for telehealth visits with voice and video. This new workflow aims to improve compatibility with voice assistants. --- [View on echobind.com](https://echobind.com/work/teladoc-building-the-future-of-healthcare-with-voice-assist) --- # Hoag Healthy Together > We helped Hoag Hospital create a single application to encompass all of the different patient journeys. Based in Orange County California, Hoag Hospital is a healthcare company aiming to revolutionize what it means to be a hospital by providing state of the art amenities and forward thinking programs that bring together all there is to being healthy. The Concept ----------- Hoag's goal was to change the way and support patients. We wanted to expand upon a patients journey and incorporate their entire healthcare experience into one mobile application. Regardless of what type of patient they were at this moment, maternity, pediatric, or general, we wanted the experience to be curated and complete. The Problem ----------- Hoag hospital is revolutionizing what it is to be a hospital and building an app to accompany that mentality was a challenge. We were especially mindful that this app would engage patients at a variety of stages in their healthcare journey (general care, maternity, pediatric to start) and we wanted to be sure the app was useful and engaging to keep them coming back. On top of this we wanted to create a curated experience for each patient with a WordPress backend. The Solution ------------ We ended up understanding a patient as always being on the Hoag journey as a whole, but that their journey changes based on their current healthcare needs. We designed and built a solution which would allow use of the application without even logging in. You can see all that Hoag has to offer including classes, news, and care options. Once logged in the users experience begins to align with their personal journey. They can begin registering for classes and events. They also gain access to what we have deemed Healthcare Modules. These modules are where the curation of the journey really begins. Users will access the module they are experiencing either by themselves or by a permission given to them by their doctor. Once they have activated a module their entire application experience begins to change. The home screen will dynamically change based on the patients journey; with maternity information during pregnancy, such as how far along you are and how big your baby might be at this point in the pregnancy. The Outcome ----------- We ended up with an app that engages patients on multiple levels. Patients are able to sign up for classes at Hoag such as yoga or pilates. They can schedule these for either in person or virtual. When a virtual class was scheduled a YouTube live link would be provided in the mobile application and once the class time has passed it could be converted to an on-demand class to watch at the patients leisure. We also used this functionality to allow patients to sign up for flu shot time slots which created an incredibly smooth process and actually had more flu shot signups then previous years. We have a fully dynamic mobile application platform that allows for scalability and adaptability. --- [View on echobind.com](https://echobind.com/work/hoag-healthy-together) --- # A Change Platform > Dough is an online platform giving emphasis to women-owned businesses, and empowering their shoppers to become #WalletActivists Dough is an online platform giving emphasis to women-owned businesses, and empowering their shoppers to become #WalletActivists. They empower members to shop in women-owned businesses by incentivizing them with savings. The App's Job ------------- Echobind had the opportunity to help create a membership platform for Dough's paid subscribers. The platform allows paid-members to view discount codes and other member benefits. It also allows Dough admins to curate promo codes and show/hide content from their Instagram feed. The Problem ----------- One of Dough's major goals was to create a membership platform that integrated data from Squarespace so they could easily manage content and avoid building out a custom CMS. Dough determined early on that Squarespace couldn't meet their needs for paywall and subscription-based membership, but it was vital that their team could add new content easily to both their marketing site and the to-be-built membership platform. The membership platform needed to allow paid users to access promotional codes from partner sites. Dough members also needed direct access to a concierge via an in-app chat. Our Solution ------------ Our developers joined Dough as the Engineering department while creating the first version of the Dough platform. We later helped them interview and onboard their first engineering hire. One of our major goals was to build a cost-effective solution, that would also easily scale with any traffic Dough could throw at it. We knew that Browser extensions and a potential mobile app were on the roadmap for the future, so we prioritized GraphQL for the API layer to make these integrations easier when the time was right. Using designs provided by Dough, our team leveraged React, Node, GraphQL, and Serverless technologies to develop a stable and cost-effective foundation for the future. Dough's marketing site (built on Squarespace) powers much of the content. We: * Built a GraphQL API in Node that syncs data from Squarespace every 5 minutes. This ensures stability and performance. Data is persisted to a Postgres database. * Built the frontend in React using Next.js. * Used Emotion as a modern CSS-In-JS approach to style components. * Connected with Smooch to enable Concierge chat/SMS. * Leveraged Stripe for subscription plans and payment processing. * Added Sentry and Amplitude for error and event tracking. * Created an admin tool that allows Dough to control individual promotional codes for brands and products. It also allows admins to show or hide content from an Instagram feed. Eventually, [**Dough successfully sold their company to Juniper**](https://www.prnewswire.com/news-releases/ladies-lets-rise-introducing-dough-a-new-website-that-makes-it-easy-to-shop-women-owned-and-harness-the-power-of-wallet-feminists-300862429.html) and they merged their technology and team. We're incredibly proud of the work we did to help the Dough platform succeed. ![Screenshot of Dough's "Become a member" text and button.](https://cms.echobind.com/assets/d6d52154-655f-4251-b42b-f8f705cbd31f) --- [View on echobind.com](https://echobind.com/work/a-change-platform) --- # Helping PlayOn! Sports Scale Their Stripe Payments > PlayOn! Sports has the largest high school sports ticketing solution in the United States. With the help of Echobind's Stripe architects, they were able to scale up their Stripe implementation with confidence. [PlayOn! Sports](https://www.playonsports.com/) has the largest high school sports ticketing solution in the United States. At peak times, such as Friday night football games, they are processing over 300 transactions per second using Stripe’s Charges API. The Charges API is now considered legacy and Stripe recommends using the Payment Intents API to charge, save, and authenticate cards. PlayOn’s engineers have been working to migrate their codebase to the new Payment Intents API in preparation for a new launch of their ticketing solution. When you’re processing as many transactions as PlayOn! does, there’s a lot of risk involved in making a change like this. So as their engineers neared completion of development, they worked with Echobind to help audit their front-end and back-end codebases. Echobind provided two of our [certified Stripe Professional Architects](/partners/stripe) to perform the audit. ## Focus of Audit After an initial meeting with the PlayOn team to understand their business and concerns, it was agreed the main focus of this audit revolved around the following questions: - Does their codebase correctly implement the Stripe Payment Intents flow? - Does their codebase follow Stripe’s security best practices? - Does their codebase correctly fulfill orders after a successful payment? - Will their codebase scale with their peak volume of 300+ transactions per second? - Is their codebase PCI compliant? ## Performing the Audit Over the course of two weeks, our architects traversed PlayOn’s codebases. Our approach was to review the different pieces of the Stripe implementation: - [Payment Element](https://stripe.com/payments/elements) to build the payment user interface in web and mobile applications - [Stripe Webhook](https://stripe.com/docs/webhooks) handlers for order fulfillment - Creation of Payment Intent and handling thereafter ## Results of the Audit After reviewing their codebases, we compiled our findings into a final report that we could present to the PlayOn team. We were able to reaffirm the areas where their implementation was sound and also make actionable, meaningful recommendations that will help reduce risk and ensure their launch is successful. We are excited to see PlayOn’s ticketing solution transition to the new Payment Intents API that will provide a much better user experience for their users and an overall upgrade to their Stripe implementation. --- [View on echobind.com](https://echobind.com/work/helping-play-on-sports-scale-their-stripe-payments) --- # Delivering News in a Flash > When you’ve already reinvented news for the web, and then you have to do it again for mobile. Twice. The Problem ----------- Axios’s news site had won legions of fans for their ‘to-the-point’ editing and formatting for the web. But the browser experience wasn’t working for mobile users. The Axios Plan -------------- With a growing user base that increasingly demanded news on the move, Axios needed to offer a native mobile app. That app had to allow users to follow favorite topics, and browse breaking news. Traps and Challenges -------------------- Axios gave themselves a tight deadline to create a new product for iOS and Android. The React Native framework would allow them to build both apps at once, but Axios didn’t have enough in-house capacity to pull it off while also supporting their core infrastructure. How We Helped ------------- Echobind embedded three full-stack engineers with Axios for 8 months. We helped: * Build features for the new mobile apps * Contribute to their news feed API * Run tests and troubleshoot complex performance problems * Set up monitoring tools to track and manage post-launch performance Signs of Success ---------------- The new apps were launched on time and [received thousands of four- and five-star ratings](https://apps.apple.com/app/apple-store/id1464917429?mt=8). It's also loved by many Echobinders—it certainly never hurts to love the products you work on. --- [View on echobind.com](https://echobind.com/work/delivering-news-in-a-flash) --- # Klem Connect: Warehouse Order and Product Tracking > We built UB Klem a web application called Klem Connect to track warehouse workflows from the calendar, order, and product level. **We built UB Klem a web application called Klem Connect to track warehouse workflows from the calendar, order, and product level.** UB Klem builds custom-ordered furniture for restaurants and other organizations. Each day, UB Klem ships 5-12+ orders with up to 30 unique products that require custom piece assembly. Lucky for UB Klem, they have a strong supervisor team with deep-seated knowledge of warehouse workflows and an ingrained awareness of daily departmental needs. Historically, order tracking happens solely in these supervisors’ heads and via pen and paper, with different documenting methods. Unfortunately, this has made expanding warehouse production, training new supervisors, and covering shifts difficult, not to mention the room it leaves for human error. Recognizing this problem, UB Klem tested a custom kanban board to track projects. This created standardized outputs where supervisors could see the same lists of order due dates and product whereabouts. The downside was that feedback couldn’t be given in real-time and had to be entered manually at the end of the day by a front-office employee instead of the supervisor operating the manufacturing equipment. To resolve the issues UB Klem was encountering, Echobind built a custom web application to track warehouse workflows from the calendar, order, and product level. The idea behind this software is that each supervisor can have an iPad in their department and can easily check off when a product enters and leaves their station. The Concept ----------- The Echobind team built a custom web app solution that allows supervisors to see real-time order data on a calendar view that highlights: - shipment due date and customer details. - order priority levels. - new and in-progress order statuses. ![](https://cms.echobind.com/assets/2492a22b-5b99-4e3b-adef-65496fa607c3) UB Klem supervisors can search and filter their calendar view to only see orders important to them. For UB Klem, filters work in an OR rather than AND fashion in order to pull as many relevant orders as possible. Many filters will work to exclude as much as possible to narrow down on a specific set of information, but UB Klem found it more valuable to pull more information rather than less. Customer names are also pretty standard across the board, so instead of just having customer names within the search function, we placed in a filter so that the CEO of UB Klem could get better overviews of progress for specific clients. Users can access detailed order information from the calendar, set order priority, and see where each product in an order is in the UB Klem warehouse. Supervisors can now track when an order’s individual products are in their department and when they leave their department. ![](https://cms.echobind.com/assets/5703f6ef-ca92-411b-a322-85b8349e24f3) UB Klem’s goal was to standardize warehouse workflows and better understand the status of an order at any given time. Klem Connect offered a way for UB Klem supervisors to document what products were in their department in a central and transparent place. This presented three interesting technical challenges for the Echobind design and engineering teams. Challenge #1: Development - Syncing data between Microsoft Dynamics and the app ------------------------------------------------------------------------------- UB Klem tracks client details, order data, and shipping information in Microsoft Dynamics. Before implementing this software, UB Klem would copy information from Microsoft Dynamics, type it into a calendar program, and print copies for the supervisors. In order for Klem Connect to have up-to-date information on each order, we would need to sync with Microsoft Dynamics. Rather than allow changes from Klem Connect to transfer into Dynamics, and potentially break something on that end, we opted to only allow information to flow one way, from Dynamics into Klem Connect, and not the other way around. This greatly simplified our data flow, eliminated the risks around pushing data into Dynamics, and saved some project time, not having to create the UI elements needed to manage Dynamics data in Klem Connect. Challenge #2: Design - Products do not move linearly through departments ------------------------------------------------------------------------ One assumption before building Klem Connect was that products would move linearly through the factory. This was not always the case; not only do products take different paths through the warehouse, but the individual parts of a product can be in multiple departments at the same times. This led to a complicated discussion about the granularity needed while avoiding swamping the users in too much complexity. Instead of tracking every piece of a product, we opted for allowing users to indicate that a product is in multiple places simultaneously. We also built the system in such a way that future iterations will allow us to track products and their parts more granularly. We implemented a system to track the department status of each product in an order by attaching a list of departments to it and providing radio buttons for users to indicate the current status. This allowed for granular tracking of each product's movement through the warehouse without overwhelming users with complexity. We ensured that status updates to one product would not affect the tracking of other products. This was achieved by associating a list of departments with each product in an order so that the status of each product could be updated individually without changing the status of other products in the order. Challenge #3: Dev - Filters need to maintain until users log out ---------------------------------------------------------------- Since many supervisors only need to look at their department, we implemented a filter system that allows users to filter by customer name, department, and order priority, and save those filter selections until the user logs out. This enabled users to easily filter and find the specific orders they need, improving warehouse operations' efficiency and organization. Maintaining filters until the user logs out was critical to avoid having supervisors set filters every time they navigate to another page. Now supervisors in the rough mill department can set a filter to only see the orders they still need to complete and, at the same time, the office staff can easily search for all the orders of a single client. Challenge #4: Development/Design - Drawer statuses -------------------------------------------------- Depending on the progress that a given department made with a product or what needed to happen with a given product, it was likely that a supervisor no longer needed to see something before other people. Once they no longer needed to see a product within an order, they might never need to see it again. We made it so that supervisors could close the ‘drawer’ of a product if they no longer needed to see it. In the below image, the user has closed the status drawer of the top product but they still have the bottom status drawer open. The challenge came when we needed to save the opening and closing of a given drawer for each supervisor and make it so that a supervisor opening or closing a drawer did not open or close the drawer for someone else. Saving these individual actions became necessary since many orders might have 10+ products, but a given supervisor might only need to interact with 1 or 2 of them. Leaving the drawers open or or not saving their status would make scrolling through the list of products more burdensome and time consuming than if they could easily navigate to the product with which they needed to interact. We also chose to default products to being open since every supervisor would need to interact with a given product at least once, even if that interaction was to indicate that it was not applicable to their department. Ultimately Open → interact → Close is longer more demanding than Interact → Close, especially for orders with 20+ products. ![](https://cms.echobind.com/assets/ca3d4748-bf99-4002-8caf-8135ee18f05f) The Outcome ----------- When UB Klem first came to Echobind, they needed a way to track orders and products without a complex paper system or requiring a few individuals to have all of those order status details in their heads. They wanted their supervisors to be able to take time off without blocking the flow of products through the warehouse. And they wanted to make it easy to train new team members without them needing to learn the complex flow of products outside the departments they work in. Klem Connect provides a solution for these needs with centralized, transparent, and easy-to-use tracking of products and orders through UB Klem’s custom production warehouse. --- [View on echobind.com](https://echobind.com/work/klem-connect-warehouse-order-and-product-tracking) --- # Corporate Hospitality Aims for Faster Food > When your core service depends on your website, and it starts to slow down, you pull out all the stops. The Problem ----------- ezCater makes it easy for businesses to find and hire hospitality professionals for corporate events. But their users were experiencing slow site load times. Meanwhile, the company was also struggling to customize follow-up communications. ezCater's Plan -------------- To continue their meteoric growth, ezCater needed to overhaul their top-of-funnel experience. * A faster site would be a better experience for new users, and it would increase brand visibility through improved SEO (search engines penalize slow-loading websites) * A more flexible email marketing platform with easily accessible analytics would allow ezCater’s marketers to optimize communications with prospects and customers. Traps and Challenges -------------------- Site speed – lazy loading images and videos, plus poor performance with screen readers meant ezCater’s website scored low for accessibility and SEO on the lighthouse scale (getting 40s and 50s out of 100). Email marketing – the existing Action Mailer platform was too hard for marketers to use. It prevented them from trying new templates, or accessing performance metrics like CTR. How We Fixed Things ------------------- Echobind sped up the site by: * switching from JavaScript to CSS, which reduced the main thread work * setting a width and height for all images * adding unique IDs to elements * deferring Google Maps and Liberty Chat until users interacted with the site * pre-connecting to Google Fonts * adding aria labels to elements for screen readers. And we migrated their Action Mailer system to SendGrid, which made it easier for ezCater’s marketers to implement new email designs and layouts. It also gave them analytics tools to assess and iterate those new approaches. Signs of Success ---------------- ezCater’s lighthouse score has increased to well over 90 for desktop and 88 on mobile (up from 56 and 43). Meanwhile the marketing team is working in SendGrid and running key A/B tests on core communications. --- [View on echobind.com](https://echobind.com/work/corporate-hospitality-aims-for-faster-food) --- # Redesigning A Core UX Flow In Calibrate’s Mobile App > Echobind worked with the team at Calibrate to quickly augment and improve the progress screen, adding a visual indicator of a member's 10% weight loss goal. Calibrate is a healthcare startup that is changing the way the world treats weight. As part of this mission, Calibrate focuses on a 10% weight loss goal rather than a number of pounds. 10% helps members work toward a clinically significant, realistic goal that is relevant to their personal journey rather than an arbitrary and impossible number based on a BMI chart. Challenge --------- All of Calibrate’s marketing clearly communicated this 10% weight loss goal philosophy in order to set program expectations for members. Unfortunately, the messaging in the app fell short of reinforcing their philosophy. They didn’t create a place for a member to see their 10% weight loss goal or measure their progress in relation to that goal.
![](https://cms.echobind.com/assets/0ef1d95a-7257-45d1-98a8-eb604b32219a)
Here’s what the progress screen looked like before we got to work. The graph provides an overview of weight loss, but only in pounds, and it doesn’t do a good job of helping a member stay focused on their goal or see a summary of their progress.
Solution -------- Echobind worked with the team at Calibrate to quickly augment and improve the progress screen, adding a visual indicator of a member's 10% weight loss goal. Here’s a summary of the work we did: * Created a high-fidelity prototype * Wrote task scenarios for testing * Wrote user interview questions * Recruited participants from Calibrate’s existing member base * Scheduled and conducted sessions remotely via Zoom * Summarized our findings, finalized the prototype, and documented the feature set for engineering …all within the span of about four weeks. With the deadline for a handoff to engineering quickly approaching, we needed to work fast. We decided to run hybrid user sessions that included user interviews combined with usability testing the solutions we aimed to develop. We wanted to get a sense of how members were conceptualizing weight loss goals to ensure we were in the right ballpark, but we also needed to test early ideas and get feedback on real solutions given the time constraints. User Research ------------- We structured each session so that members’ answers to the interview questions weren’t biased by their use of the prototype. We sent a Figma prototype link to members and walked them through the task scenarios only after they had answered the interview questions.
![](https://cms.echobind.com/assets/f9623928-b785-489c-b977-8d9db0e3a72f)
Here are the interview questions we used. Warm-up questions are used to build rapport, while the core interview questions are used to uncover answers to the main research question.
Rapid Prototyping ----------------- We started out with a single prototype to get some initial feedback but quickly realized that members were unable to complete one of the core tasks because the interface didn’t clearly signify that you could horizontally scroll on the weight graph. We created a second prototype with a horizontal scroll signifier and spent the remaining sessions A/B testing each prototype.
![](https://cms.echobind.com/assets/5a9ae1e8-61a2-4443-9b30-824e04f68bee)
Changes were made to ensure the progress bar and start weight & goal weight were large enough. We also added a scroll indicator to the graph so members could easily navigate to a previous date.
This pivot highlights one of Echobind’s core strengths: We’re able to recognize when something isn’t working early on in a project and quickly correct course to find a more effective solution. Conclusion ---------- The second prototype performed much better than the first, and members were easily able to complete the task scenarios. With a solid foundation of user research and validation of base usability, we marked up the designs for handoff to engineering.
![](https://cms.echobind.com/assets/92cb7371-1800-4d26-958d-379b5f759f1d)
Our designers markup their designs with notes for development to explain complex interactions or nuanced design decisions. This saves the engineers time by limiting the amount of guesswork they have to do and saves the designers time when they QA the final build.
With this new version of the progress screen, Calibrate members are now able to easily track their weight loss progress against their 10% weight loss goal. As Echobind continues to work with Calibrate, we’re looking to monitor the performance of the new progress screen and iterate as we collect more performance data to refine design solutions. --- [View on echobind.com](https://echobind.com/work/redesigning-a-core-ux-flow-in-calibrates-mobile-app) --- # Empowering Educators, One Lesson at a Time > When your mission is to make world-class problem-solving accessible to all, you build a platform that’s as innovative as the educators who use it. The Problem ----------- Brilliant is all about turning people into world-class problem solvers. They provide a learning platform for those who want to master quantitative skills—think math, data, and the ever-growing fields of CS and AI. They deliver interactive lessons that cut out the fluff. The team behind Brilliant is over 100 strong, with folks based in San Francisco, NYC, and scattered across the globe, all working together to make learning as impactful as possible. **Brilliant for Educators** is an extension of this mission, offering free access to Brilliant’s rich content to K-12 students and teachers worldwide. It’s not just about access, though—Brilliant for Educators provides educators with tools to invite students, organize classes, assign lessons, and keep tabs on student progress. But with all this functionality tied up in Brilliant’s core platform, they needed a serious revamp to give Brilliant for Educators the space it needed to grow. **Client Need:** The client needed to decouple both the onboarding experience and the educator admin tools. Brilliant needed to break Brilliant for Educators out of its current mold and build it as a separate entity, all while keeping it connected to the main platform. The challenge was to do this without disrupting what was already working, especially since Brilliant for Educators was tangled up with the rest of Brilliant’s codebase. They were looking for a partner to help bring this vision to life and ensure that Brilliant for Educators could stand on its own while still playing nicely with the rest of Brilliant. The Plan -------------- Our solution? Create a brand new Next.js application specifically for Brilliant for Educators. This way, we could lift and shift the necessary parts from the existing Vue 2 and Next.js codebases, ensuring everything looked and felt like Brilliant, but with the freedom to innovate and expand Brilliant for Educators without stepping on the main platform’s toes. Traps and Challenges -------------------- This wasn’t your typical “start from scratch” project. Brilliant for Educators was already established and deeply intertwined with Brilliant’s core platform, so separating it out had challenges. We had to be super careful to avoid breaking anything in the process, especially when moving from Vue 2 to Next.js. Consistency was key—everything had to look and feel just right to make the transition seamless for users. How We Fixed Things -------------------- - **Frontend Development:** We built the new Brilliant for Educators platform using Next.js and React, with Chakra UI handling the styling. This combo allowed us to create a responsive, sleek interface that mirrored the main Brilliant site, making sure users felt right at home. - **Backend Development:** On the backend, we used Next.js alongside Supabase for the database. We utilized RLS (Row level security) from Supabase which fostered a “security first” approach. For deployments, we leaned on Vercel to streamline the process. ![](https://cms.echobind.com/assets/5f814e54-fc08-4237-b9de-d24bca415479) Signs of Success -------------------- **Result/Impact:** We’re proud to say that Brilliant for Educators v2.0 was launched on schedule, just in time for the new school year. Educators now have a platform that’s not only easier to use but also packed with powerful tools to manage their classes and content. Brilliant for Educators is now a shining example of how educational tools should work—intuitive, powerful, and built to grow with its users. ![](https://cms.echobind.com/assets/6156bb52-bc37-4b7b-97ad-00a7c72208a0) Future Scalability -------------------- **Looking Ahead:** Now that we have decoupled the platform from Brilliant, there are more opportunities to independently expand and enhance the tool with educators and students in mind. Brilliant for Educators not stopping here; there’s plenty more on the horizon: 1. **Enhanced Progress Monitoring:** We’re working on real-time updates that will let teachers see exactly how their students are progressing, down to the minute. 2. **Content Navigation Enhancements:** We’re refining the content navigation tools with improved UI, better search functions, and more accurate tagging—making it even easier to find the perfect lesson. 3. **Educator Portal UI Improvements:** We’re giving the Educator Portal a facelift, creating a more cohesive and seamless experience from start to finish. ![](https://cms.echobind.com/assets/80d0a563-bed0-42a9-b364-f1de88d9f5b3) --- [View on echobind.com](https://echobind.com/work/empowering-educators-one-lesson-at-a-time) --- # Rental Property Payments Made Simpler > The Dvora app was already valuable to a large number of building residents; moving their payment system to Stripe made it even more so. [Dvora](https://www.dvoralife.com/), a resident-services company, has a mobile app that is designed to make life hassle-free for tenants of their buildings. Residents can book building services, discover and RSVP to events, reserve spaces, and more. Challenge --------- As the app grew in complexity, Dvora realized they needed a payment system that gave more control and flexibility to residents, landlords, property managers and service providers. Their different users needed to be able to do things such as: * accepting payments from their residents without having to leave the app * accommodating multiple custom user flows for payments and receivables * scheduling future rent subscriptions and custom proration * applying automatic late fees to subscriptions with varying configurations * allowing residents to have control over automatic vs. manual charges on a per subscription basis * allowing residents to make payments to different businesses without having to re-enter a payment method for each business It was clear that Dvora had outgrown a one-size-fits-all payments approach and needed a tailored solution. Solution -------- As a Stripe-certified team, the Echobind team was able to understand all of Dvora’s challenges and business functionality requirements to develop payment flow diagrams and user flows.  Mapping this out for the Dvora team allowed us to validate and determine which Stripe products were necessary. We determined that developing a secure and scalable microservice that contains REST API endpoints to interact with Stripe’s API would be the best solution. We utilized the following Stripe products during our implementation: * [Stripe Connect](https://stripe.com/connect): helps landlords and property managers with smoother payouts to service providers. * [Stripe Elements](https://stripe.com/payments/elements): provides the ability to collect payments directly in Dvora’s app without leaving the app for a seamless experience. * [Stripe Billing](https://stripe.com/billing): allows for managing subscriptions and generating invoices for residents.
![](https://cms.echobind.com/assets/3a8be6a1-df6f-4272-a8fa-e7362ca4d87d)
Result ------ With their newly rebuilt payment system, the Dvora app is now a first-of-its-kind platform that allows landlords, property managers, and service providers to easily make and receive payments customized to their specific needs. In the end, all parties that used the app now had what they needed: * Service providers were able to receive direct charges into their Stripe Connect account and were responsible for managing refunds and disputes * Landlords and property managers could split payments between multiple deposit accounts * Renters were able to easily pay one lump sum and know that their payments were being distributed accordingly—without having to make multiple payments to various accounts. Last but not least, the payment solution we built relied on Stripe to do all of the heavy lifting (communicating with card providers, facilitating transactions and all of the other great things Stripe can do). This allowed Dvora to keep focusing on their central mission: making the lives of residents easier. 💡*Visit our [Stripe partnership page](/partners/stripe) to learn more about Echobind’s expertise in building Stripe implementations.* --- [View on echobind.com](https://echobind.com/work/dvora-stripe-rental-property-payments) --- # Helping Bose Ship > Echobind stepped in to salvage the existing code base on this Bose project and get the campaign live. Bose is a manufacturing company that predominantly sells audio equipment. Bose is best known for its home audio systems and speakers, noise-canceling headphones, professional audio products, and automobile sound systems. The Engagement -------------- Echobind teamed up with Bose to improve sales for the Home Speaker 500 through a 30 day free trial period. During our 3 month engagement, we collaborated with marketing, product and backend teams to deliver various HTML templates that would fit within their Java based platform. We worked with their marketing team to define the why. We helped their product team scope the necessary requirements. And we enabled their backend team to effectively integrate the frontend solution. The Problem ----------- The goal of the campaign was to increase product adoption through a physical free trial of their Home Speaker 500. The hypothesis for our specific team was that offering free trials to a targeted audience with a compelling on-boarding experience would create higher conversion and more engaged usage. The project had been in development for the past 18 months and it was the first of its kind— the usage of Bose's microservices in this fashion was unprecedented. The Solution ------------ Prior to our engagement, a plain HTML/CSS/JavaScript and jQuery solution was in place. However, there were multiple negative UX side effects and various security vulnerabilities were discovered during our initial audit. We considered porting existing code to React and patching API requests with a JWT workflow. However, we determined that keeping the codebase and focusing on high priority bugs was in the best interest of the project's projected delivery. ![A webpage showing a shipping details form](https://cms.echobind.com/assets/0bb19f30-2544-453e-ae23-08826dc5fb51) The Outcome ----------- The campaign was delivered two weeks ahead of schedule and the conversion rate was around 95% - 98%. This successful turnout enabled the marketing teams at Bose to continue to evaluate free physical trial periods with other products. The successful integration with their internal microservices also lead to future discussions around opening up further integrations to allow teams to rapidly iterate over these bleeding edge marketing concepts. --- [View on echobind.com](https://echobind.com/work/helping-bose-ship) --- # Building a Home Health Mobile App from Scratch for Abby Care > Echobind partnered with Abby Care, a home health provider for children with special needs, to develop AbbyAid, an Expo powered React Native mobile app that enables caregivers to complete daily charting with Electronic Visit Verification, biometric authentication, and Medplum integration. Abby Care is a home health provider for children with disabilities or special needs. Abby Care trains family caregivers for certification, develops personalized care plans for each child, and then compensates the caregivers through Medicaid for their services in caring for their child. In 2024, they recognized the need for a mobile application to better serve their community and extend their reach through digital channels. With a clear vision but needing technical expertise, they turned to Echobind for end-to-end mobile app development. ## The Need Abby Care needed a mobile application, AbbyAid, built from the ground up to allow caregivers to complete daily charting for their child. The organization required a charting flow that included: 1. Electronic Visit Verification (EVV), Biometric authentication, Location tracking, Recording of start and end time of charting 2. Handle dynamic and complex routing depending on the care plan created for the caregiver, and whether the charting is being completed on time or late 3. Integration with Medplum The project needed to move from concept to reality quickly, requiring both initial development expertise and a commitment to long-term partnership. ## The Solution ![AbbyAid Login Screen](https://cms.echobind.com/assets/62a469d7-58ac-439e-809b-10d390026d46) The team chose **React Native with Expo** as the technology stack, providing several advantages: * **Cross-platform development**: One codebase for both iOS and Android. This also allowed Abby Care’s web developers to contribute to the codebase. * **Rapid development**: Expo's toolchain accelerated the development process and publishing to the App Store and Play Store. * **Future-proof architecture**: Built-in support for over-the-air updates and easy maintenance ### Technical Highlights: * Built entirely from scratch using React Native + Expo * Implemented a scalable architecture to support future growth * Guided the Abby Care team through the App Store and Play Store internal testing processes to getting the app reviewed for publishing ![AbbyAid Mobile Apps](https://cms.echobind.com/assets/edda341b-6efa-4501-a7ba-ad65a4447e55) ## The Outcome The partnership resulted in: * A fully functional mobile application from concept to production in two months available in the App Store and Play Store * Establishment of an ongoing support relationship, demonstrating client satisfaction * A scalable foundation that allows Abby Care to continue evolving their digital presence * Seamless knowledge transfer to the Abby Care team to enable them to maintain the codebase going forward The success of the initial build led to continued engagement, with Abby Care investing in additional development hours after the app had been in production for months to audit, upgrade to the latest Expo version, and expand the application's capabilities. >> **"We loved working with Echobind, and they left us in a great spot."** -- Evaline Bai, Director of Engineering ## Why Echobind? Abby Care chose Echobind for several key reasons: * **Full-stack mobile expertise**: Deep knowledge of React Native and Expo enabled rapid, high-quality development * **Healthcare expertise**: Having prior experience allowed us to make critical recommendations, and move faster when implementing features like EVV and working with FHIR data standards. * **Collaborative approach**: Regular communication and flexibility in scheduling showed commitment to client success * **Long-term partnership mindset**: Rather than just building and walking away, Echobind reengaged with Abby Care months later to audit their recent changes and resolve some of their pain points in the codebase * **Professional project management**: Clear communication channels and proactive updates kept stakeholders informed throughout the project This project exemplifies how we take a mobile app from initial concept through development and into successful long-term support, providing clients with both technical excellence and reliable partnership. --- [View on echobind.com](https://echobind.com/work/building-a-home-health-mobile-app-from-scratch-for-abby-care) --- # Scaling Fertility Care with Self-Service Scheduling & EMR Automation > Discover how CNY Fertility cut onboarding time by 87% and slashed wait times from weeks to days through automation and self-service scheduling. # How CNY Fertility Cut Patient Onboarding Time by 87% and Slashed Wait Times from Weeks to Days ## When your intake form becomes a business blocker, it's time to build something better CNY Fertility's team remembers the moment everything changed. After months of outgrowing their existing form solution, they watched as their new system automatically pulled patient data and populated their EMR in seconds. "It's magical," one employee shared, watching 40 minutes of manual data transfer and appointment coordination compress into an automated process. This is the story of NOVO, a ground-up patient onboarding platform that transformed how CNY Fertility connects with patients seeking care. ## About CNY Fertility CNY Fertility is a fertility clinic founded by Dr. Robert Kiltz in 1997, known for offering affordable and accessible fertility treatments. With locations across the US, including Syracuse, Albany, Buffalo, Rochester, Atlanta, Colorado Springs, Philadelphia, and Sarasota, they aim to make fertility care more attainable for a wider range of individuals. CNY Fertility is recognized for its inclusive practices, offering financing options without credit checks and accepting patients regardless of age, BMI, or ovarian reserve markers. ## The catalyst for innovation Due to their reputation for accessible, affordable care, demand for CNY Fertility's services was rapidly outpacing their operational capacity. Like many successful healthcare providers experiencing rapid growth, CNY Fertility recognized the need to evolve their systems. Their CEO had ambitious plans to dramatically increase consultations, but achieving this vision required reimagining their entire patient onboarding infrastructure: - Their existing form solution had reached its limits, causing operational slowdowns. - Like many healthcare providers, CNY faced the common challenge of manual data entry taking up to 40 minutes per consultation. - Appointment scheduling required multiple staff touchpoints and back-and-forth communications, stretching wait times to over a month. - Rescheduling meant more phone calls, emails and staff time. - SMS costs had grown to over $10,000 monthly through their EMR's messaging system. The math was simple: to scale consultations, they needed to completely reimagine their onboarding infrastructure. ## The partnership When evaluating scheduling solutions for the project, Echobind knew that typical HIPAA-compliant scheduling services weren't designed for the unique workflows of fertility practices. Rather than compromise on functionality, Echobind's CEO reached out directly to SavvyCal's CEO, Derrick Reimer, to discuss the possibility of creating a new scheduling application specifically for appointments. As longtime users of SavvyCal for meeting scheduling, Echobind was thrilled when Derrick and the SavvyCal team expressed enthusiasm for tackling the challenge of building a HIPAA-compliant solution. This collaboration gave birth to [SavvyCal Appointments](https://savvycal.com/appointments), a new product line designed specifically for healthcare's unique scheduling demands. For CNY Fertility, it meant getting a purpose-built solution that could handle their needs. For SavvyCal, it represented an exciting expansion into the healthcare market with a trusted partner to help validate and refine the product. ## Building NOVO: The technical innovation ### The challenge of no API Most modern integrations rely on APIs, but CNY Fertility's EMR system offered none. This constraint pushed Echobind to think creatively, ultimately implementing robotic process automation (RPA) using UiPath that would become NOVO's secret weapon. ### The tech stack Echobind built NOVO on a foundation designed for healthcare's demanding requirements: - Ruby on Rails 8 for rapid, reliable development. - PostgreSQL for robust data management. - HIPAA-compliant architecture with encryption at rest and in transit. - Stripe for secure payment processing. - SavvyCal for intelligent appointment scheduling. - UiPath RPA for EMR integration without APIs. - Modern Rails stack including Hotwire, Turbo, and Stimulus for responsive interfaces. ### Design through data The team went through multiple design iterations, but what set this project apart was the commitment to continuous improvement. They added HIPAA-compliant product analytics to monitor the patient journey and elevate metrics to key stakeholders, creating a feedback loop that revealed drop-off points and optimization opportunities. ## The transformation ### By the numbers After just 8 weeks of development, the results exceeded even the most optimistic projections: - **From several weeks to within days:** Average consult wait time dropped from several weeks to just 2 days. - **87% less clinical workload:** 40 minutes of data transfer and scheduling coordination streamlined to just 5 minutes, with plans already underway to achieve full automation. - **Higher patient satisfaction scores:** Self-service scheduling eliminated frustrating phone tag. - **100% self-service scheduling:** Patients could now book, reschedule, and cancel without any human intervention. - **Zero manual payment processing:** Fully automated, secure payment collection. ### An unexpected bonus: massive cost savings While the primary goal was improving patient experience and staff efficiency, switching from the EMR's SMS system to AWS SNS delivered an additional win: over $10,000 in monthly savings. This 90% reduction in messaging costs meant the project would pay for itself rapidly while improving message delivery and engagement. ### The game-changer: true self-service scheduling Perhaps the most transformative aspect of NOVO was what it eliminated entirely: the need for human intervention in scheduling. Patients gained complete control: - Book instantly based on real-time provider availability. - Reschedule anytime before their appointment. - Automatic slot recovery when rescheduling, immediately freeing spots for other patients. This shift from staff-managed to patient-managed scheduling didn't just save time. It fundamentally improved the patient experience while freeing CNY Fertility's team to focus on clinical care rather than administrative tasks. ## That magical moment For CNY Fertility's staff, watching the RPA system work for the first time was transformative. Forms submitted by patients automatically flowed into the EMR, data fields populated perfectly, and appointments synced in real-time. But the real magic happened when they realized patients could completely manage their own scheduling journey. Booking, rescheduling, and canceling without a single phone call or email. What once consumed nearly an hour of administrative time per patient now happened automatically in the background. ## The unexpected challenges of success Success brought its own surprises. The dramatic increase in consultation volume surfaced new operational challenges that CNY Fertility had never encountered at their previous scale. These "fast follow" issues, while good problems to have, required quick adaptations to maintain service quality at the new volume levels. ## Looking forward: the future of healthcare onboarding NOVO's success at CNY Fertility points to a massive opportunity in healthcare technology. The challenges they faced—legacy EMR systems without APIs, complex scheduling requirements, HIPAA compliance needs, and manual data entry burden—plague every telehealth provider, from primary care and mental health practices to dermatology and specialist consultations. But the opportunity extends beyond healthcare. Any business that relies on scheduled paid consultations (coaching, legal services, financial advisors) faces similar challenges with intake forms, scheduling, and payment collection. NOVO's architecture provides a blueprint for transforming these experiences across industries. The Echobind team's commitment to continuous improvement drives their roadmap forward: "We're actively working to eliminate even those last 5 minutes of manual work," says Michael Yared. With enhancements to the RPA capabilities and deeper EMR integration already in development, NOVO is positioned to become the standard for healthcare onboarding. ## The bigger picture What started as a growth opportunity for one fertility clinic has evolved into a blueprint for modernizing healthcare operations. NOVO proved that even with legacy systems and complex compliance requirements, it's possible to create patient experiences that feel magical while dramatically improving operational efficiency. For healthcare organizations struggling with similar challenges, NOVO demonstrated that the choice isn't between maintaining legacy systems or complete digital transformation. Sometimes, the most powerful solution is building a bridge between the two. **See how building a custom application can help your organization cut onboarding and scheduling time by 80% or more. Contact Echobind today for a free consultation.** --- [View on echobind.com](https://echobind.com/work/scaling-fertility-care-self-service-scheduling-emr-automation) --- # Simplifying the healthcare experience for pet owners > Koala Health is on a mission to make the pet care experience easier for Pet Parents. We helped them move this goal further by allowing them to offer custom telehealth solutions for new and existing customers. --- [View on echobind.com](https://echobind.com/work/simplifying-the-healthcare-experience-for-pet-owners) --- # An Evidence-Based App to Help MS Patients with Depression > Pear Therapeutics developed the first FDA-authorized Prescription Digital Therapeutic (PDT). Echobind worked with them on 006, an app designed to treat depression in patients with MS, which entered Pear's clinical study pipeline. Pear Therapeutics was known for producing the first smartphone app to receive FDA authorization for treating medical conditions. They received FDA clearance for doctor-prescribed apps to treat substance use disorder and opioid use disorder, and were working toward clinical applications to help treat a number of other conditions. Echobind leveraged several of their senior engineers to help the Pear Therapeutics team research, develop and publish their app, simply named “Pear 006”, whose goal is to treat depression in Multiple Sclerosis patients. The App’s Purpose ----------------- Depression is a common symptom for multiple sclerosis patients, who have their own set of concerns, worries and biological symptoms. The depression symptoms associated with MS are known to be very manageable with the help of Cognitive Behavioral Therapy (CBT), a widely-used treatment methodology in which patients make note of their automatic negative thoughts, and replace them with more constructive ones. The Pear 006 app took the form of a chatbot that would help a patient form the habit of assessing negative thoughts and turning them into more constructive ones. Each iteration of the app was validated with patients to ensure it was going to be maximally helpful to them. Our Solution ------------ Our process with the Pear team involved regular assessments of goals for both the company and the end users, combined with evaluations of feature development possibilities. For the CBT chatbot, we conducted a comparative analysis of existing frameworks like Microsoft Bot Framework, Rasa, and Google’s DialogFlow, carefully weighing the pros and cons of each through the lens of client concerns and making informed recommendations. Echobind leveraged its expertise in AI to create a custom chatbot for Pear 006, designed to support MS patients with depression. By developing a proprietary chatbot "brain" that efficiently managed patient interactions through guided pathways, Echobind ensured minimal developer intervention while allowing Pear's mental health experts to fine-tune content. This bespoke solution not only streamlined DevOps overhead but also provided a scalable platform for future AI enhancements, demonstrating Echobind's capability to integrate advanced technology in healthcare applications. Ultimately, the right solution was different than expected: the Echobind team built a custom chatbot “brain” that was contained within the app, directing the patient through different chat threads based on their input. Each conversational thread was completely administered by the Pear 006 expert team via a sophisticated yet stable spreadsheet interface. This design ensured the chatbot brain could be easily replaced if needed. In short, Pear was able to continually refine and improve the chatbot’s patient interactions with minimal developer involvement. --- [View on echobind.com](https://echobind.com/work/an-evidence-based-app-to-help-ms-patients-with-depression) --- # O'Neil Software solutions for record storage centers > O’Neil Software, specializing in software solutions for record storage centers, partnered with Echobind to develop Annex — a first-of-its-kind e-commerce platform to amplify the reach of local record centers and transform how individuals find businesses to handle off-site record management. ### The Opportunity O’Neil Software’s existing system adeptly manages record centers’ client data and inventory, but they saw an untapped opportunity: create a marketplace. A consumer-facing e-commerce model had yet to be created in the industry. So they thought, “What if we pivot everything on its head and go after the real customers of storage: The everyday user, and not simply warehouses?” This creates a largely-untapped opportunity for an e-commerce spin on what is traditionally a very pen-and-paper acquisition model. Annex was conceptualized to bridge this gap, offering a platform for record centers (existing O’Neil Software clients) to advertise services and enabling consumers to easily purchase storage space for physical files and media services. Attracting new customers to long-term storage contracts with new record centers allows record centers to secure new recurring revenue over time without heavy advertising and marketing costs, and Annex benefits from that by being the majority provider of services to that network. And just like that, an entire industry was made instantly more accessible and user-friendly. ### Echobind Solution: Consumer-facing e-commerce web application ![](https://cms.echobind.com/assets/c6dab65b-71dd-4896-8daf-57997950e2ea) The O’Neil Software team enlisted Echobind’s expertise in designing and building a modern, user-friendly interface for Annex. The platform not only solidified O’Neil Software’s customer retention but also opened new revenue streams for record centers. ![](https://cms.echobind.com/assets/6da09932-e0f1-47a5-bc65-7d37851369ac) ### Key annex.com Features: - **Responsive Web Application**: The initial release features a responsive web design, with plans for major mobile app development in the very near future. - **Tools & Technologies**: Utilization of modern tools such as Turborepo, Next.js, Directus, GraphQL, Tailwind, Expo, Nativewind, tRPC, Storybook, Tailwind Variants. - **Payment Integration**: Incorporation of Stripe for seamless payment processing. - **Innovative Backend**: Integration with internal APIs using tRPC for stable and efficient production deployments. - Backend development involved connecting to their internal API and using tRPC to automatically set up the types to connect to both the Directus GraphQL API that we were using and the internal Annex API. Having stable and defined types and endpoints that were consistent and predictable allowed for issues to be found very early in the development process, resulting in very stable production deployments requiring zero rollbacks after deploy. - The Annex team's adoption of Directus enables non-technical members, especially in marketing, to independently manage and update website content without coding, enhancing responsiveness to market changes and reducing reliance on the technical team for regular content updates. - **Frontend Excellence**: A mix of web and mobile interfaces, focusing on cross-platform compatibility and reusability. - Frontend was a mixed web and mobile interface utilizing Storybook, Expo, Nativewind, Tailwind, and Tailwind Variants for cross-platform "build once" functionality. Any components designed to be shared across web and mobile were built to be independent of any platform-specific needs and thus reusable`.` ![](https://cms.echobind.com/assets/0396c148-54f3-449c-88fc-1a2c221756df) ## The Design: Echobind conducted rapid-iteration workshops, resulting in swift and effective design outcomes. Real-time decision making and feedback facilitated rapid validation with the team. During the discovery workshop, the O’Neil Software and Echobind teams identified additional requirements: - **Content outline:** The Annex team internally crafted an initial design wherein they delineated the content they deemed crucial for their company. This served as an initial blueprint for the website's content. - **Content Management System:** The Annex team required the capability to control, update, and create new content for their business. - **E-commerce element:** Clients needed the ability to shop for storage goods, find and select storing vendors, agree to service agreements, enter payment details, and check out. - **Account management:** Users needed to be able to view and manage their orders, stored goods, invoices, order statuses, review vendors, and manage other users. - **Integrations marketplace:** A library of software that can easily connect to the Annex software to enhance the user's experience. ![](https://cms.echobind.com/assets/db95b122-5570-4136-9863-8283368a016c) The design solution to these requirements included: - Custom pages and elements for easy content management, eliminating the need for extensive design efforts from the Annex team - Modules that can be personalized with content by the client, enabling them to create future pages and update existing content conveniently - A personalized shopping experience integrating the purchase of goods, vendor selection, and service agreements seamlessly - A user portal enabling account management, viewing of order statuses, documents, and managing authorized users - A marketplace showcasing logos, providing information, and offering a selection of external solutions to fulfill diverse client needs ![](https://cms.echobind.com/assets/1aaf966e-16b4-4a46-bccf-10019f011507) O’Neil Software and Echobind’s collaboration in developing Annex resulted in a significant leap in e-commerce for the record storage industry, showcasing Echobind’s ability to deliver innovative, robust, and user-friendly software solutions. ![](https://cms.echobind.com/assets/0fa85571-711f-436e-b1b8-ecd854e233b8) ![](https://cms.echobind.com/assets/6eda76f9-c6e1-4894-84b7-00c81bed0239) ![](https://cms.echobind.com/assets/832eb54e-ada2-452d-bc09-fd97de5fb6bd) --- [View on echobind.com](https://echobind.com/work/oneil-software-annex) --- # Upstream: Expanding Contraceptive Care Access for Rural Communities > Echobind partnered with Upstream, a patient-centered contraceptive care nonprofit, to build FreeBC. Upstream partners with healthcare organizations to improve contraceptive care education and access. Together with Echobind, they built FreeBC, a digital product designed to reach the people most underserved by traditional clinic access: women in rural communities, where the nearest provider can be hours away and private, judgment-free information is hard to find. > "You guys are so fast. I have never worked with a dev team this fast. There is no messing around with you guys." ### The challenge For many women, the barrier to contraceptive care isn't just cost. It is access and information. Rural communities often have few clinics, long travel times, and limited private ways to ask honest questions about birth control. FreeBC set out to close that gap: help someone learn about their options, compare methods, get trustworthy answers, and find a pathway to actual care, all from a phone. ### What we built Echobind built a live digital care product that takes a user from "I have questions" all the way to fulfillment: - **Education and method comparison.** Clear, accessible resources that let users understand and compare birth control options side by side. - **A personalized quiz.** It recommends methods based on a user's lifestyle and preferences, which turns a confusing decision into a guided one. - **Connie, a clinician-reviewed AI assistant.** Trained on clinician education materials and QA-reviewed for medical soundness, so users can ask private questions and get answers they can trust. - **Video testimonials.** Real stories that make an intimidating topic feel human. - **Telehealth fulfillment and clinic referral.** Pathways to care that work even for users without reliable phone access, plus clinic search and referral workflows. - **Detailed product analytics.** PostHog dashboards track how users move through education, decision, and fulfillment, so the product can keep improving on real behavior. ### How we worked Echobind moved fast and shipped a live product, working closely enough with the Upstream team that speed became the thing they remembered most. Connie in particular required tight collaboration with clinicians. The AI assistant is only useful if it is medically sound, so clinician review was built into how it was created and maintained rather than bolted on afterward. ### The outcome FreeBC is live and moving real users through the full pathway, from learning about options, to getting personalized guidance, to fulfilling a method through telehealth. The product gives Upstream a scalable way to extend contraceptive care education and access to communities that clinics alone can't reach, with the analytics foundation to prove what's working and expand from there. --- **Building digital health that has to earn trust?** [Let's talk.](/contact) --- [View on echobind.com](https://echobind.com/work/expanding-contraceptive-care-access-ai-assistant-telehealth-platform) === # Blog # Building a React Native POS App with Stripe Terminal and Tap to Pay > Lessons from shipping a React Native POS app with Stripe Terminal, Tap to Pay, a BFF over two backends, and a payment flow built to prevent duplicate charges. _By Preston Kelly · 2026-07-10_ We recently shipped a mobile POS app for a client whose staff take payments in the field. It had to accept contactless payments on a phone, present one experience over two existing platforms, and recover safely when a charge succeeded but the network response failed. Those constraints drove most of the architecture. ### Tap to Pay means the phone is the reader Dedicated readers add friction to a field rollout. Staff have to keep them paired, charged, and nearby. With Stripe Terminal's React Native SDK, a supported iPhone becomes the contactless reader. Staff can accept cards and phone wallets without carrying another device. We also support the BBPOS WisePOS E and the pocket-sized M2 for teams that need chip and swipe. Both options use the same [Stripe Terminal React Native SDK](https://docs.stripe.com/terminal) integration. The app asks Terminal to collect a payment, while the connection layer handles the reader choice. Two things worth knowing going in: - Tap to Pay and Bluetooth readers only work on physical devices. Plan for on-device testing early, because you can't validate the real payment path in a simulator. - You will manage runtime permissions. Terminal needs Bluetooth and Location. Handle the case where a user revokes them mid-session, not just the first-launch prompt. ### One app, two backends: reach for a BFF Our client runs two platforms with different APIs and data models. The app sends every request through a small Node and Express backend-for-frontend. The BFF uses session context to route the request and normalizes the response. The React Native app receives one contract for records, catalogs, and payments. Platform-specific field names and missing endpoints stay in the BFF, where we can test them without spreading conditional logic through the UI. ### The payment flow that can't double-charge The riskiest failure happens after Stripe accepts a payment but before the app receives confirmation. If the staff member retries, the customer could be charged twice. We use the PaymentIntent ID as the transaction's identity from creation through sale recording. The backend records at most one sale for that ID: ```js POST /payments/record { "paymentIntentId": "pi_123", ... } // Server: has pi_123 already been recorded? // yes -> return the existing record, do nothing else // no -> record it, then return ``` A network retry now returns the existing sale instead of creating another one. The device also stores the last successful PaymentIntent ID, so the app can recover after a crash without starting a new charge. Automated tests cover this exact failure path. If a future change allows the same PaymentIntent to create two sale records, CI catches it before release. ### OAuth with PKCE and native secure storage Staff sign in with OAuth 2.0 and PKCE, which avoids storing a client secret in the mobile app. The app stores session tokens in native Keychain or Keystore storage through Expo. The backend receives the authenticated session context it needs to route each user to the correct platform. ### What we would repeat - Start testing Stripe Terminal on physical devices early. A simulator can cover the interface, but it cannot validate the payment hardware path. - Use the PaymentIntent ID as a durable transaction key across the device and backend. - Keep multiple upstream platforms behind one BFF contract. - Lead with Tap to Pay, then support dedicated readers for teams that need chip or swipe. Building something similar, whether that is in-person payments, a POS on mobile, or a unified app over multiple systems? [We'd love to help.](/contact) --- [View on echobind.com](https://echobind.com/post/in-person-payments-stripe-terminal-tap-to-pay-react-native) --- # Echobind's Second Product: Meet CopperTab > Echobind is launching CopperTab: HIPAA-compliant billing for cash-pay healthcare. Why we built it, who it's for, and why we're the team to do it. _By Michael Yared · 2026-07-08_ For over a decade, Echobind has built software for other people. Health systems, telehealth platforms, clinics, comsumer companies, startups. We take on work that challenges us, and we're good at it. But every so often, client work shows you the same problem enough times that you stop billing hours against it and start building the answer. That's what happened here. Today we're announcing [CopperTab](https://coppertab.com), a new company that provides HIPAA-compliant billing and payment infrastructure for healthcare providers. It's the second product to come out of Echobind, after Kind Kiosk launched in 2024, and it exists because of a gap we kept hitting in our healthcare practice. ## The problem we kept running into ![](https://cms.echobind.com/assets/7a493bc7-0688-4980-970c-f6756530150a) Healthcare is going cash-pay fast. GLP-1 and weight loss programs, fertility treatment, direct primary care, hormone health, longevity medicine, med spas, concierge practices. These businesses run on memberships, payment plans, and recurring charges. They look a lot more like modern subscription companies than like traditional fee-for-service medicine. The billing tools haven't caught up. Modern payment processors are exceptional at moving money, and they are deliberately not in the business of handling protected health information. That's the right call. But it leaves providers stuck: the moment a subscription describes a treatment, a plan name, or anything that ties an identifiable person to their care, you have PHI in your billing system. General-purpose billing platforms either aren't built for that at all, or they gate HIPAA compliance behind enterprise contracts that a twelve-provider clinic will never sign. We know this because our clients kept asking us to solve it. And we did, over and over, as custom builds. A compliant billing layer here, a PHI-safe checkout there. Custom HIPAA payment infrastructure routinely costs six figures to build and then has to be maintained forever. At some point, building the same thing repeatedly for individual clients stops making sense. It should be a product. ## What CopperTab is ![](https://cms.echobind.com/assets/975d7041-4ec8-413a-9bba-dd14f872edd1) CopperTab is subscription billing, invoicing, and payment infrastructure built specifically for healthcare. The core idea is simple and architectural: PHI never reaches the payment processor, by design. CopperTab holds all of the sensitive context. Patient records, plan names, billing logic, invoices, retry schedules, subscription terms. When it's time to collect a payment, only a neutral, PHI-free charge goes downstream to the processor. Providers connect their own Stripe account, funds settle directly to them, and CopperTab is never in the funds path and never the merchant of record. And CopperTab signs a Business Associate Agreement with every client, on every plan. Not as an enterprise upsell. Every plan. That's paired with a platform that runs on HIPAA-compliant infrastructure with encryption, access controls, and audit logging throughout. The pitch to a provider fits in one breath: keep your payment processor, keep your rates, and let CopperTab handle the compliant billing layer so you never have to think about PHI leaking into a payment system. ## Who it's for CopperTab is built for cash-pay and patient-pay healthcare: weight loss and GLP-1 clinics, fertility centers, direct primary care, hormone and men's health, menopause care, longevity and executive medicine, medical aesthetics, and concierge practices. If your revenue is recurring and your patients pay you directly, CopperTab was built for you. It works just as well for multi-location groups that need one compliant billing layer across every clinic. ## Why we're the right team to build it Three reasons. **First**, we've lived in [healthcare software](https://echobind.com/industries/healthcare) for years. We've built for telehealth platforms, clinics, and health systems, integrated with the major EHRs, and shipped products that run in hundreds of care settings. We know where PHI hides, and we understand the workflows healthcare companies depend on to get paid. **Second**, we know the payments side. Echobind is a [Stripe Premier Partner](https://echobind.com/partners/stripe), and we've implemented complex payment infrastructure across all types of client engagements. CopperTab isn't our first time connecting healthcare to modern payment rails. It's the productized version of work we've already done many times. **Third**, we built compliance in from the first commit. CopperTab was never a prototype that got a compliance program bolted on later. Isolated per-client databases, audit logging, encryption, and access controls were part of the architecture from day one, because in healthcare the architecture is the compliance story. CopperTab is its own company, with Echobind as its engineering partner. That separation is intentional: healthcare clients deserve a purpose-built entity whose entire job is this one thing. ## What's next CopperTab is live at [coppertab.com](https://coppertab.com), and we're onboarding our first providers now. If you run a cash-pay practice and billing compliance is top of mind, [we'd love to talk](https://echobind.com/contact). --- [View on echobind.com](https://echobind.com/post/echobind-s-second-product-meet-copper-tab) --- # Building a Healthcare AI Chatbot: Meet “Connie”, the Contraceptive Care Chatbot > Building an AI chatbot for contraceptive care means navigating empathy, privacy, and clinical boundaries all at once. Here's how we built Connie, and what we learned along the way. _By Emily Nadler · 2026-06-24_ How do you build a chatbot that is empathetic, personalized, and private, which are three things that AI chatbots notoriously struggle with? When a healthcare client approached us to build an AI chatbot specialized in contraceptive care, we had to face this question head on. Healthcare conversations require towing the line between providing patient-centered educational resources without crossing into clinical decision-making territory. Here's what I learned, as a Product Manager on the team that built “Connie,” an educational chatbot for a digital contraceptive healthcare product. ## Understanding the client’s needs Like any good software project, the first step in building the AI chat was understanding what the client wanted to achieve, and how it would fit into the rest of the digital product. This part felt familiar, even if the technology behind it was new to me. What would success look like? How would this chatbot fit into the larger experience? ### Personalization and contextualization One of the client's most important requirements challenged the generic chatbot paradigm entirely. They didn't want users to always simply be greeted with "Hi, what can I do for you?" Instead, they wanted Connie to be contextually aware of the current page that the user was viewing. Imagine you just clicked into the page with information about the IUD. When you open the chatbot and ask, “tell me about this,” Connie responds: "I see you're looking at the Hormonal IUD. Would you like to ask a question about side effects or insertion?" That's a fundamentally different (and better) user experience. We track which pages the user is on, and which pages the user has visited. Including that in the chatbot prompt allows Connie to say things like, "I noticed you were curious about IUD pain. Would you like to discuss pain management options?" This approach transforms a reactive help tool into a proactive guide that meets users where they are in their research journey. ### Patient-enabled decision making tone Trust is everything in healthcare. Providers are trained to maintain a patient-centered, empathetic, and calm tone of voice while giving care. Because we were working within the context of contraceptive care, which has its own host of stigma, it was even more important to us to think critically about gaining the trust of a potential user interacting with an AI chatbot. Rather than writing tone guidelines from scratch, we used actual physician training materials as part of the chatbot's context. We also provided examples of "good" versus "bad" contraception support language, with specific phrases to use and avoid. A core design principle was making the chatbot feel like a knowledgeable friend or sister rather than a clinical authority. By allowing casual language, affirming phrases, and even the occasional emoji, the experience feels approachable and human. Connie meets users where they are instead of where a doctor's office expects them to be. The anonymity of a chatbot adds another layer of comfort, removing the self-consciousness that can come with discussing sensitive topics face-to-face. For women who find conversations about reproductive health intimidating, that combination of warmth and privacy can make all the difference. ## Security and Privacy Are Non-Negotiable Healthcare data requires extreme caution, even in a public educational tool. We had to address several security concerns: Session Management: We store conversations server-side rather than in the browser to prevent users from editing their message history and manipulating the chatbot into violating its safety guidelines. HIPAA Compliance: We treated HIPAA compliance as a foundational requirement throughout the project, which meant thinking carefully about potential data exposure at every step of the process. Because the application does not register users, users are not identified as they navigate the site. Additionally, each chatbot session gets a unique token, so there is no persistent tracking across sessions. Beyond the user-facing design, we evaluated our entire infrastructure for compliance risks. This included ensuring our LLM provider has a BAA in place, and configuring our analytics and error-tracking tools to avoid logging identifying details. FreeBC is fully HIPAA compliant, so chat histories are stored in a HIPAA-compliant database. ## Testing is key The client wants their team, their clinical team, and our internal team to actively try to "break" Connie before MVP launch. The most helpful feedback format we established was: Query: What the user asked Response: What the chatbot said Expected Response: What it should have said This structured feedback loop helped us iterate on the prompt engineering and refine the chatbot's expected behavior. One helpful decision we made was to separate the system prompt such that the chatbot on the production site used its own file, completely isolated from staging. This allowed the engineering team to iterate freely on the system prompt themselves in staging leading up to the MVP launch, without any risk of affecting the live product. What this testing looked like in practice was hands-on and iterative. It involved testing system knowledge, adjusting the chatbot’s tone, and refining edge cases. It was deliberately a team effort of trial and error: each session surfaced gaps or unexpected behaviors, which were addressed by refining the system prompt. ## The Bottom Line The process of building healthcare AI chatbots successfully comes from respecting three things: your users' context, your domain experts' knowledge, and your users' privacy. Most importantly, remember that in healthcare a chatbot isn't just answering questions, it's supporting people through potentially life-changing decisions. That responsibility should guide every design choice you make. --- [View on echobind.com](https://echobind.com/post/building-a-healthcare-ai-chatbot) --- # Beyond the Build: How We Helped Upstream Understand Their Product Through Analytics > How Echobind built an analytics layer for FreeBC, Upstream's contraceptive care platform, helping a nonprofit see how real users move through their product. _By Emily Nadler · 2026-06-24_ When a product launches, the work doesn't stop at deployment. The real questions start arriving: Are people using the product? Which flows are they using the most? Where are they dropping off? For Upstream, a nonprofit focused on expanding access to contraceptive care for underserved women, answering these questions and adjusting the product as needed was critical to achieve their mission. As a product manager at Echobind, I was positioned well to understand how to create the tooling in order to help take our engagement with Upstream beyond feature delivery and build a window into how their users actually experience their platform, FreeBC. ## The Platform: "FreeBC" FreeBC is a digital tool designed to guide users through contraceptive education and connect them with care. The application includes a “quiz” flow, which guides users through questions aiming to capture their preferences surrounding lifestyle and priorities for birth control, and provide them with results based on methods best for them. It also has an AI chatbot, trained using materials used to train clinicians. It also has 19 method detail pages, which contain information from clinicians regarding pros and cons of the method, video testimonials from real users, and pathways for both telehealth and clinic-based care completion. Upstream had meaningful questions they needed answered before and after launch: - Which campaigns were driving the most traffic? - Were users completing care pathways in a single visit or across multiple sessions? - Were lower-income users gravitating toward telehealth or clinic-based care? - Which methods were most popular? - Were users actually engaging with the AI? - Where were users dropping off in the quiz, the method exploration, and the telehealth flows? The list was long, and it reflected a team that genuinely wanted to understand their users, with the goal of ultimately improving the product. ## PostHog PostHog was already in the stack, as it’s Echobind’s analytics tool of choice. The challenge for us wasn't access to data, it was how best to structure that data. Raw event data flowing into PostHog is only useful if you've thought through what you want to capture, how you want to group it, and what questions those events are meant to answer. The first phase of the work was translating Upstream's business questions into an analytics architecture: funnels, insights, and dashboards that could surface real answers. Some questions mapped naturally to PostHog's out-of-the-box capabilities. Conversion funnels became the primary tool for understanding the telehealth and clinic care pathways. These helped us track where users entered, where they advanced, and where they left. Drop-off points in the quiz, method exploration, and care flows were modeled as distinct funnel stages so the team could see exactly which steps were losing users. Other questions required a bit more creativity. Tracking scroll depth, CTA clicks by location on the page, video engagement, AI chatbot interactions, and filter usage meant thinking carefully about what events were being fired and whether they'd hold up over time. ## The Event Tracking Decision Early in the process, the team went through three distinct phases of thinking about event tracking, and each phase taught something the previous one couldn't. **Phase one: autocapture.** PostHog offers autocapture out of the box, which automatically records user interactions without any developer instrumentation. It requires near zero work to set up, and an immediate flow of data. The team started here, using autocaptured events to begin building funnels and insights. This worked until the limitations became apparent. Autocaptured events are opaque. You can see that something was clicked, but tying it reliably to a specific interaction, across users, across languages, across product changes, is fragile. **Phase two: interaction IDs.** The next step was attaching interaction IDs to PostHog events. We manually tagged elements in the codebase so that events could be identified more precisely and used more generally across insights and funnels. This felt like a meaningful improvement. Instead of relying on PostHog to guess what happened, the team was asserting it from the level of the codebase. But the cracks started to show as the team went deeper. The core problem was brittleness. Interaction IDs are scoped to how components are structured in the code. If a developer refactors a component, even without changing any functionality, the interaction ID can change silently, breaking any insight or funnel built on top of it. And because developers have no visibility into what PostHog is tracking through these IDs, they wouldn't even know they'd broken something. There was also a multilingual dimension. FreeBC currently supports both English and Spanish users, with the goal of expanding to more languages in the future. Interaction IDs tied to text content would fail to capture Spanish-language interactions consistently, meaning any funnel built on them would have a hidden gap in the data. **Phase three: custom events.** The team landed on fully custom events: deliberate, developer-placed tracking calls that fire when specific things happen in the app. More setup work upfront, but dramatically more reliable. When a developer sees a custom event in the code, they know that interaction is being tracked, and if they change the component, they know to preserve it. The tracking is no longer silent to the people maintaining the codebase. The team also designed the custom events to be dimensional rather than exhaustive. Instead of creating a separate event for every pill on every method page, a single `pill_clicked` event carries properties like `method` and `prompt_text`. Instead of a hundred CTA events, a single `cta_clicked` event with `placement` (e.g., `home_hero`, `home_get_real`) and `target_url`. One event, many dimensions, and fewer things to break. ## Building the Dashboards With a cleaner event architecture in place, the dashboard work could begin in earnest. The dashboards were organized around Upstream's core questions: **Acquisition** — Where are users coming from? UTM source and medium tracking gave visibility into which campaigns drove traffic, and how those sources correlated with deeper engagement versus immediate bounces. **Engagement** — How are users actually moving through the product? Page view tracking, scroll depth, and CTA click events made it possible to understand not just that users visited a page, but how they interacted with it. Funnel visualizations showed the most common paths users took before completing a care pathway. **Care Pathway Conversion** — The telehealth and clinic funnels were the heart of the analytics work. Each stage of each flow was instrumented, so Upstream could see exactly where momentum was lost. Were users dropping off when asked for their zip code? After viewing their best-fit method? Before completing the telehealth intake? These answers would directly inform product iteration. **AI Engagement** — The AI chatbot was a major feature investment, and Upstream wanted to know if it was paying off. Tracking chatbot opens, the pages users were on when they engaged with it, and the volume of back-and-forth exchanges gave a picture of how the AI was being used — and whether users who engaged with it were more likely to complete a care pathway. **User Demographics and Outcomes** — Some of the most meaningful questions Upstream had were about equity: Were lower-income users completing care differently than higher-income users? How did age correlate with method selection and care completion? These questions intersected with a separate data mapping effort, but the PostHog instrumentation was designed to leave room for that analysis. ## What This Kind of Work Actually Takes One of the things I took away from this engagement is how underestimated analytics work tends to be, especially on product teams moving fast toward launch. Setting up event tracking isn't a checkbox. It requires product thinking (what questions matter?), technical judgment (how do we structure events so they're maintainable?), and ongoing coordination between PMs and engineers. We were doing a lot with a little, and that constraint forced some real clarity. It pushed us toward the custom events approach, which helped us create analytics dashboards that would hold up over time as the product evolved. ## The Bigger Lesson Upstream's mission is to make contraceptive care accessible to women who face the most barriers to getting it. Every drop-off in the quiz, every user who didn't complete their care pathway, every AI interaction that didn't lead somewhere represent real people who came looking for help and didn't quite get there. Building the analytics layer helps to serve that mission. It gives Upstream the visibility to ask hard questions about their product, learn from the answers, and keep improving. Delivering a product is the beginning. Understanding how it's actually used is what makes it better. --- [View on echobind.com](https://echobind.com/post/beyond-the-build-through-analytics) --- # Manage Personalized AI Slack Agents Across Your Org > How Echobind built a private, personalized Slack AI agent management app. _By Stuart Gross · 2026-06-22_ We've been quietly running a per-person AI agent experiment at Echobind where each teammate has their own Slack AI assistant. Each assistant has its own bot identity, its own memory and its own learnings (and eventually it's own token usage tracking). The agents live on a private tailnet so we can `ssh` in as needed, while provisioning and lifecycle are handled from a small internal tool we built called **Roster**. This post is the story of how we built it, why we picked the technologies we did, and what the UX taught us along the way. We want to share what we learned since portions of our work is made public and open-sourced (the Railway template and agent container repo). And If you're thinking about embedding agents into your own team's workflow, this demonstrates what we might build for you too. The stack overview: - **[Hermes](https://github.com/NousResearch/hermes-agent)** - the open-source agent runtime from Nous Research. Persistent memory, skills, learning, multiple chat-platform gateways out of the box. - **[rome-on-rails](https://github.com/echobind/rome-on-rails)** - Echobind's [Railway template](https://railway.com/template/) that wraps the [upstream Hermes Docker container](https://github.com/NousResearch/hermes-agent/blob/main/Dockerfile) with a Tailscale sidecar, an opinionated entrypoint, and a documented env-var contract. - **[Tailscale](https://tailscale.com/)** - runs inside the container, so each agent is its own tailnet node we can SSH into, with no public surface. - **Roster** - Echobind's internal control platform with an org chart of people plus the agents assigned to them. It contains the "Create new agent" flow that drives the whole Railway provisioning sequence and lifecycle control. ### Why per-person agents, and what that meant for the design We didn't want one agent for the whole company. We wanted personalized agents with there own: - **Slack identity.** A distinct bot name, avatar, and Slack app, so the assistant feels personal and their context is not cross-pollinated with other bots. - **Memory.** Conversations, skills, and learned preferences that don't bleed across teammates. (Hermes's `state.db` lives on a per-service Railway volume) - **Access boundary.** Only the people on a specific Slack allowlist can talk to a given agent and the list can be updated any time. - **Maintainer surface.** When something breaks, we need the ability to `ssh` in and look at logs and inspect files directly. Those four constraints shaped almost every decision. ### The two-layer access model One Hermes Railway service per Slack agent, behind tailnet was our chosen model. It lets team members chat with their agent without ever knowing what Tailscale is, while an engineer who needs to debug an agent's memory or rotate a skill can quickly ssh in without exposing anything to the public internet. Decoupling those two surfaces was important and Tailscale turned out to be the right tool for the job. > *Screenshot: the Roster 'agents' dashboard.* > >![](https://cms.echobind.com/assets/368957ff-e3d8-433d-8356-941e383be8ca) ## The Railway template for Hermes Slack agents Before we built Roster, we built **rome-on-rails**. It's a Railway template that deploys a single Hermes agent and tailnet node. It pulls the upstream Hermes Docker image (we purposely designed around the upstream image so we can benefit from future updates with minimal maintenance), adds a small entrypoint that sets up Tailscale networking, runs Tailscale with ssh, and then hands off to the upstream Hermes entrypoint. It also persists agent memory on the Railway volume so an agent keeps the same tailnet hostname and memory across redeploys. A big design decision for the Railway template was to stay deliberately single-service since Railway templates fix the service count at publish time. So a 3-service template is wrong wouldn't work for a client who needs 2 or 4 agents, and publishing a per-count template means maintaining multiple near-identical templates with different service counts. So instead, the template provisions one Hermes service, and to add another agent in the same Railway project, we duplicate the service inside Railway (in our case, programmatically using the Railway API). All we have to do is override the per-agent environment variables (Slack tokens, Tailscale hostname, LLM provider key) but leave Railway workspace token and the Tailscale auth key alone since they are shared at the project level. Per-agent variables on the service with shared variables on the project means we have granularity over control of things like LLM token usage. ## Postman as the empirical validator Before integrating the Railway API into our frontend, we took the "trust but verify" approach to the API docs. We built a Postman collection first, ran every query against the live system, watched the Railway dashboard and the logs update live , and only then translated the verified shapes into our backend. This also enforced our understanding of the API interactions as we iterated our Postman environment and queries until they were successful. It also let us create a Postman collection json artifact customized to our app that we could leverage using AI later on. ## The Roster UX With the API verified and the template stable, we built the Roster side. Roster is the place we manage who has agents and which humans they support. The idea is an org chart of people with Slack agents assigned as supporting resources for those people. > *Screenshot: the Provision modal: top half.* > > ![](https://cms.echobind.com/assets/a116f2da-4aa8-4034-97fe-0811a83e0ef3) The "Create new agent" button on the Agents page opens the provision modal where the user enters the required information. The prompt and notes fields don't ship to Railway, they just live on the agent row in our database. The "Project" toggle is where the operator decides whether to spin up a brand-new Railway project for this agent or add the agent into an existing project. > *Screenshot: the Provision modal: bottom half.* > > ![](https://cms.echobind.com/assets/1df9ee69-9505-43ab-a7b0-c236c3e5d89c) This is where the per-agent environment overrides live. The LLM provider key, the two Slack tokens, the home channel, the Slack Allowed Users checkbox list. Only checked users can talk to the agent in Slack and nobody else. The Slack user ID list is controlled in a table that is seeded by our team, so the user picks from real humans, not free-text member IDs. Once **Provision agent** is clicked, Roster does the following: 1. Creates a new project or uses an existing project's ID. 2. Generates a per-project Railway token via and persists it in our DB with encryption. This is what lifecycle calls (start/stop/restart) authenticate with afterward instead of the workspace token. 3. Fetches the published Hermes template, adds the user's inputs to setup the Railway environment variables, and deploys the template. 4. Persists the new agent to the database with status set to provisioning. 5. Returns immediately so we aren't waiting for Railway to finish building. Since a Railway build can take several minutes, we close the modal the moment Railway has accepted the deploy, show the new agent card in the dashboard as "provisioning", and only update the card to "running" *after* the deployment webhook arrives back at Roster. > *Screenshot: the post-provision handoff. The one manual step left.* > > ![](https://cms.echobind.com/assets/da9caf5e-fd27-4f1f-a481-f6230caaf18f)(Pasted%20image%2020260502131142.png) There's one manual step left, paste the per-project webhook URL into Railway's dashboard. Railway's GraphQL API doesn't (yet) expose programmatic webhook configuration, we discovered this suring Postman development, planned for it, and made sure the modal hands the URL off as a simple one click copy-and-paste option. > *Screenshot: the project detail page, where that webhook URL lives forever.* > > ![](https://cms.echobind.com/assets/6d53b288-01a7-413d-a991-3079b0854bc0) The project detail page is where the operator can come back and grab the webhook URL again, regenerate it (treat as a credential), or watch the green "Webhook live" indicator confirm that Railway is talking to us. The page also lists every agent in the project with an available link to its detail view. ## Agent lifecycle control via buttons Once an agent is provisioned, it's lifecycle is controlled by just three buttons on the agent card: **Start**, **Stop**, and **Regenerate token** (decommission button is coming in a future revision). Under the hood: > *Screenshot: agent card for lifecycle managment* > > ![](https://cms.echobind.com/assets/4961c4f0-30e4-4d0f-bbd5-5efe942c12ec) - **Start** calls the deployment restart endpoint against the agent's last deployment Id. The container comes back with the same volume, so same Hermes session history, same skills, same Tailscale machine identity, same MagicDNS hostname. The agent picks up where it left off. - **Stop** calls the stop deployment endpoint. While the container is paused, Slack and Tailscale go quiet, but the data volume is preserved. Every one of those actions is observable in Roster via the webhook. The status of each agent flips through `provisioning → ready → running → stopped` based on what Railway actually says happened, not what we *think* should have happened. ## How to use the Railway template yourself Roster itself is internal Echobind tool, but the parts you need to build the *agent* side are public: - **The template:** [github.com/echobind/rome-on-rails](https://github.com/echobind/rome-on-rails) has the Dockerfile, the entrypoint, and template-notes that describe the environment variables. - **The Readme:** contains the one-click Railway template deploy button . - **The docs:** the rome-on-rails repo includes tailscale-setup.md, secrets-guide.md and webhook-setup.md so you can deploy your first agent in no time. - **Hermes upstream:** [github.com/NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent) credit to the Hermes runtime; we just package it. ### Common gotchas - If SLACK_ALLOWED_USERS is left blank, the bot looks online but never responds. - If SLACK_BOT_TOKEN and SLACK_APP_TOKEN are reused accross services, two containers connect to the same Slack app and there is no way to specify which agent receives the message. - If TS_HOSTNAME is not unique, machines fight for the same hostname in Tailscale; only one wins, the others are unreachable. Always set an explicit hostname. - Choose Reusable Tailscale auth keys since non-reusable keys spend themselves on the first registration and the agent can't re-register after a redeploy. - When testing webhooks in local dev, Railway can't reach your laptop. We used [ngrok](https://ngrok.com/) to provide a tunnel for the webhook to reach our local dev and put the public URL in NEXT_PUBLIC_APP_URL. ## Future considerations A few things we're excited about in the next iteration: - **Programmatic Slack-app creation:** Today, every new agent needs a Slack app created by hand (bot token, app token, install to workspace). We're going to explore the [Slack App Manifest API](https://api.slack.com/reference/manifests) to provision Slack apps automatically as part of the "Create new agent" flow. - **Editable allowed-users list:** The provisioning modal already has the right checkbox UX for picking allowed Slack users. We're going to show the same control on the agent detail page so moving users between existing Slack agents is easy. - **Per-agent token usage and live cost:** OpenRouter and the upstream providers expose usage data per request. We want to easily see how much agents have been spending and what their allowed to spend in Roster. For now the spend column is seeded, but we hope to have this live soon. If any of this is interesting to your team, whether that's "we want our own per-person agents," "we want to build something like Roster for our own runtime," or just "we want to talk through whether AI agents are the right shape for our workflow", we'd love to [hear from you](https://echobind.com/contact). And if you want to play with the agent side without us, the template is available. Deploy it, paste in a Tailscale auth key and a Slack bot tokens, and you'll have your own personal Hermes assistant on your tailnet in minutes. The hardest part is picking what to name it. *Special thanks to [Nous Research](https://github.com/NousResearch/hermes-agent) for the open-source Hermes runtime that this is all built on, and to the [Railway](https://railway.com/) and [Tailscale](https://tailscale.com/) teams for platforms that enable us to experiment with new ideas.* --- [View on echobind.com](https://echobind.com/post/manage-ai-slack-agents) --- # We Built a Slack AI Agent on Hermes. Here's Everything That Broke. > Build a Slack AI agent with Hermes Agent, from a locked-down droplet to persistent company knowledge. Includes the toolset security config, a two-layer context file setup, and four gotchas that aren't in the docs. _By Stuart Gross · 2026-03-25_ During my first internship in 2012, a coworker shared the company's "acronym bot" to help me get up to speed. It was a large company with a LOT of acronyms, and I still remember what a lifesaver that integrated chat bot was. Fast forward to 2026, and the value of a customized AI-powered chatbot within an organization goes well beyond fetching acronym definitions. Today's AI assistants can perform complex tasks using natural language prompts, schedule and perform recurring tasks, create deliverables, or even change their behavior under the hood for a better user experience. Today we'll implement a custom AI-powered Slack app that responds when @mentioned inside a channel it's been invited to. We'll use an open source AI framework called Hermes Agent to power our app. This is our framework of choice because it's lightweight, works with our LLM of choice including a local LLM, and because it supports the features mentioned above. Tailscale will serve as our private network provider to ensure our server is only accessible by us. ### Spin Up a Linux Server and Lock It Down Before we can install anything, we need a server that's always on and reachable by Slack. A cloud virtual machine is a great fit for a proof of concept project of this scope. We'll use a DigitalOcean Droplet to host our Linux server with the 2GB RAM option. This gives us some headroom on top of required processes (Linux, Hermes framework, observability dashboard, SQLite session, etc.) which would creep dangerously close to the 1GB plan. The setup is straightforward. We pick a region, an operating system, CPU and storage specs, paste our SSH key, and we're ready to go. For the OS, we're going with the Ubuntu long term support image since Hermes docs use Linux commands throughout their documentation and the FAQ section even recommends Ubuntu. Once the Droplet is created, we SSH in, create a non-root user with sudo privileges to work from, run `sudo apt update && sudo apt upgrade -y` to get everything current, and reboot. Now we've got a fresh server on DigitalOcean with a public IP address. A fresh Linux server accepts connections on any port by default. We want to make sure SSH key authentication is the only way in. DigitalOcean handles this when we create a Droplet using an SSH key by disabling password login automatically. We'll use [Tailscale](https://tailscale.com/) to limit who can attempt SSH key authentication. Tailscale creates a private encrypted network between our devices using the WireGuard protocol. Think of it like a VPN, but we don't run a VPN server. Instead, Tailscale handles the coordination, and the actual traffic is encrypted end-to-end between our devices. We install it on the server, install it on our laptop, sign in with the same account, and now they can talk to each other over private IPs that nobody else can reach. Now with Tailscale up, we configure the server's firewall (`ufw` on Ubuntu) to deny incoming traffic by default. Then create an exception to only allow traffic arriving through the Tailscale interface. Outbound traffic is allowed since the server needs to call external APIs (Slack, our LLM provider, package repositories). So now even if someone knows our server's public IP address and tries to connect on any port, the connection gets dropped. ```bash sudo ufw allow in on tailscale0 sudo ufw allow 41641/udp sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw enable ``` ### Set Up the Hermes Agent Framework on the Server [Hermes Agent](https://github.com/NousResearch/hermes-agent) is an open source AI agent framework that gives our LLM tools persistent memory and messaging integrations. It extends chatbot capabilities to executing tasks, scheduling cron jobs, searching the web, reading files, and remembering context across conversations. It supports multiple LLM providers including local models. For our LLM provider, we went with [OpenRouter](https://openrouter.ai/) with Claude Sonnet 4.5 as our model. OpenRouter lets us access models from multiple providers (Anthropic, OpenAI, etc.) through a single API. [Installation](https://hermes-agent.nousresearch.com/docs/getting-started/installation) is a one-liner: ```bash curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash ``` The installer handles everything: it installs Python, clones the Hermes repo, installs all dependencies, and launches a setup wizard. The wizard walks us through picking our provider, model, terminal backend, session settings, and messaging integrations. It was honestly very easy to set up following the wizard, but hand-editing the YAML config is also a valid option. After the wizard finishes, we verify the install with `hermes doctor` (which checks API keys, tools, and config) and a quick test chat to make sure the model responds. Troubleshooting: the setup wizard wrote the model config as a flat string. ```yaml model: anthropic/claude-sonnet-4.5 ``` But Hermes actually expects a nested YAML key. ```yaml model: default: anthropic/claude-sonnet-4.5 ``` When left flat, Hermes falls back to a different default model. In our case it defaulted to Opus 4.6. ### Create the Slack App and Connect to the Server via WebSocket This step follows the [official Hermes Slack setup guide](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/slack). We begin by creating our Slack app in the Slack API dashboard, then configure the bot token scopes, and grab the two required tokens for Hermes. Socket Mode needs to be enabled since we prevent incoming traffic. Without it, Slack is unable to send messages to our server. WebSocket mode allows the server to initiate two way communication with Slack. ![Slack API dashboard showing Socket Mode enabled for the Hermes app](https://cms.echobind.com/assets/19dd72e1-c97b-4398-86b5-4f9ebd97dbbc) With our Slack app set up, we can add the Slack API tokens to our server environment variables and install the [Hermes Gateway](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/?_highlight=gateway). We'll install it as a systemd service (`hermes gateway install`), and enable "lingering" so the service keeps running even after we close our SSH session. The gateway is what actually maintains the WebSocket connection to Slack and routes messages to and from the Hermes agent. ![Terminal output confirming the Hermes gateway systemd service is installed and running](https://cms.echobind.com/assets/1948e972-a29d-4b3f-a531-90c15731466d) Once the gateway is started, we're ready to test the @mention flow in Slack. We just need to install our app in Slack, and then invite Hermes into a channel. We can designate a Slack channel as Hermes' "home" channel where it will log activity by default. ![Hermes bot replying to an @mention in a Slack channel for the first time](https://cms.echobind.com/assets/5ef89589-7dc8-4bf4-a694-56a3723836e2) Troubleshooting: During initial gateway testing, the Slack bot was triggering an "active session" interrupt on every first message after startup and preventing the gateway from responding. After some troubleshooting and verifying the Slack app and Hermes environment were set up correctly, we checked the Hermes Agent GitHub repo. We noticed `slack.py` had been modified in a recent batch of commits with changes that seemed relevant, so we ran `hermes update`, which pulled 98 new commits. Then we restarted the gateway, and the issue was gone. This was a great reminder to check for updates frequently when working with source code that is moving fast. ### Explore Slack Security Guardrail Options Hermes comes with a lot of built-in [toolsets](https://hermes-agent.nousresearch.com/docs/user-guide/features/tools): web search, cron jobs (scheduled tasks), terminal command execution, file operations, code execution, memory, and more. Out of the box, it's powerful. But in some applications we may not want our Slack bot to run shell commands on the server, manipulate files, or spawn sub-agents. Hermes uses a `config.yaml` that acts as an allow list, so only the toolsets we choose are available on the platform. ![config.yaml showing the per-platform toolset allow list restricting which tools Slack can access](https://cms.echobind.com/assets/351ad631-7c62-4aad-a847-2315755f2728) Troubleshooting: Hermes has a global `toolsets` setting that defaults to `[all]`. If you don't change this to `[]`, it overrides the per-platform restrictions and makes everything available everywhere. We discovered this when Hermes made code changes via Slack requests even though the Slack toolsets were not listed. Another powerful layer of security is the `SLACK_ALLOWED_USERS` environment variable. It is a user whitelist that only allows listed users to make Slack requests to Hermes. Slack user IDs are available under Slack user profiles. Every user ID has to be added to the Hermes environment variables before that person can make requests, including our own. Hermes also has some baked in [security](https://hermes-agent.nousresearch.com/docs/user-guide/security) guardrails to protect against other forms of threats. In our use case, the user whitelist along with the toolset config offers enough protection. But it's good to understand the defense in depth offered by Hermes for other applications where guardrails need to be tighter. ### Set Up Hermes Agent Context Files Since we want Hermes to always "know" about Echobind, we need to decide how to set up its knowledge internally by referring to the [Context Files](https://hermes-agent.nousresearch.com/docs/user-guide/features/context-files/) documentation. Since the "How Context Files Are Loaded" section mentions the overall context is "injected" into our prompt, we want to minimize the overhead of company knowledge that gets sent with every prompt. To do this, we'll take advantage of the Hierarchical Discovery feature when loading context files. On startup, it walks recursively from its working directory and concatenates every `AGENTS.md` file it finds to build its context. So we'll use a two-layer approach. A condensed summary of Echobind will live in `~/.hermes/knowledge/AGENTS.md`. This gets automatically loaded into every prompt and gives Hermes an overview of the company along with where to find more detailed information. The full company content, scraped directly from Echobind.com in this case, lives as individual markdown files in the same `~/.hermes/knowledge/` directory. When a user asks a specific question, like "tell me about the Stripe partnership", Hermes uses its `read_file` tool to pull the relevant files that don't get loaded with every prompt. Troubleshooting: Hermes also maintains its own `~/.hermes/AGENTS.md` file where it writes self-improving context over time in the root directory. This file doesn't exist at initial setup, and so the hierarchical discovery fails until it is either manually created, or Hermes creates it. Also, we don't want our Echobind knowledge to be overwritten, so we don't store it in the root directory `AGENTS.md` file since Hermes updates this as its context updates. We instead store it in the knowledge directory (created by us) `AGENTS.md` file where it is discovered hierarchically. Finally we need to populate Hermes' knowledge with content from Echobind.com. We'll use a custom Python scraper to pull web content around services, case studies, capabilities, and partnerships. These are the pages that answer the question "what does this company do?" The final knowledge directory created by the scraper looks like this (each section contains relevant markdown knowledge files). ![Directory listing of the ~/.hermes/knowledge folder with markdown files grouped into sections](https://cms.echobind.com/assets/0203966d-4995-41b9-b8f3-85ef0703f722) ### Test Persistent Knowledge and Memory This is the moment of truth. Does Hermes actually use its knowledge instead of searching the web? We'll test from the command line first with a request for general information. ```bash hermes chat -q "What does Echobind do?" ``` Hermes should pull from the scraped content without using tool calls at this point since the Echobind summary in `AGENTS.md` will handle this. ![Terminal output of Hermes answering "What does Echobind do?" directly from context with no tool calls](https://cms.echobind.com/assets/8558e28a-4a69-45f3-a28f-1ccbe6a4fdaf) Next, let's test something more specific, like "Give me a detailed description of Echobind services." This information lives in an individual knowledge file, not in the summary `AGENTS.md`. So Hermes will use its `read_file` tool to pull the relevant file(s) and answer with details about the services. This will confirm the two-layer system is working since the summary provides the general overview, and the detailed files get pulled on demand when specifics are needed without searching the web. Let's test this using Slack to get the full user experience. ![Hermes in Slack calling its read_file tool to answer a detailed question about Echobind's services](https://cms.echobind.com/assets/30695374-bd94-417f-87a2-c5a61d49447f) Over time, Hermes also builds up its own learned context in its self-managed `AGENTS.md`. As it handles more questions, it adapts by recognizing patterns across sessions and updating how it handles similar questions. This is part of the "adapts to you" piece. The knowledge gives it context, but the persistent [Hermes Memory](https://hermes-agent.nousresearch.com/docs/user-guide/features/memory) lets it get better at using it. Users can also explicitly tell Hermes to adapt with prompts like, "always format your responses to me using bullet points" or "use relaxed but professional language in this channel". ![Hermes in Slack acknowledging a formatting preference and applying it to the following response](https://cms.echobind.com/assets/91a58bbd-3273-4d9e-beb0-c16099b17e05) The last toolset we'll look at is `schedule_cronjob`, one of the most useful tools Hermes offers. We'll have it perform a simple task here for the purpose of this walkthrough. But it is capable of scheduling and executing complex recurring tasks. Let's ask Hermes to remind us to check our email in 5 minutes. ![Hermes in Slack scheduling a cron job and delivering the reminder five minutes later](https://cms.echobind.com/assets/569b892c-0aea-4cca-abe9-fecc4c0edb0b) ### Create an Observability Portal We need to know if the agent is working, what it's been doing, and what went wrong when something breaks. Without a dashboard, the only way to check on Hermes is to SSH into the server and review logs manually. Instead, we'll build a lightweight Flask dashboard since we're already using Python and Flask is a Python web framework. The dashboard is simple with only two routes. The root (`/`) serves the HTML page, and `/api/status` returns a single JSON payload with everything the frontend needs. The frontend fetches `/api/status` on a timer and refreshes the page every 30 seconds. No database, no WebSocket, no client-side framework, just a page that polls a JSON endpoint. The dashboard will show whether Hermes is up or down, system resource usage (memory, disk, uptime), recent activity parsed from the gateway logs, recent errors and warnings, and the current state of the knowledge files. We'll also set it up to run as its own systemd service that automatically starts up, and make it only accessible over Tailscale. ![Flask observability dashboard showing Hermes status, system resource usage, and recent gateway activity](https://cms.echobind.com/assets/e52133b9-6414-48fa-b925-eb271f900829) Fun Fact: We built the initial observability dashboard, then enabled file and terminal command toolsets for Slack to experiment with them. When prompted, Hermes updated the dashboard to add additional observability without any issues. We probably could have let it build the initial dashboard as well. ### Final Thoughts Hermes is a long way from the acronym bot I used during my internship over a decade ago. But the core idea is the same. It's an integrated tool that lives where your team already communicates and helps them get things done. The difference now is that the tool can reason, remember, adapt, and perform tasks instead of just looking up definitions. We went from an empty DigitalOcean Droplet to a fully functional AI Slack assistant that is secure, connected to Slack, and knows about our company. We also highlighted some troubleshooting and design considerations along the way. Hermes Agent is currently a quickly moving project, but the barrier to entry is not as high as it first seems. **What it costs** - Hermes Agent is open source and free. - DigitalOcean Droplets start at $12/month for the 2GB tier we used. - Tailscale has a free tier that's more than enough for a setup like this. - LLM usage is the variable line item. Token spend scales with how often your team @mentions the bot, so watch it for the first few weeks. **Resources** - [Hermes Agent on GitHub](https://github.com/NousResearch/hermes-agent) - [Hermes Slack integration guide](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/slack) - [Tailscale](https://tailscale.com/) - [OpenRouter](https://openrouter.ai/) --- [View on echobind.com](https://echobind.com/post/hermes-agent-slack-setup-guide) --- # Beyond Compliance: What We Learned Building an App for Adults with Intellectual Disabilities > Key lessons learned building a financial literacy app for adults with intellectual disabilities, including visual-first design, dual-user interfaces, and gamification as essential accessibility strategies. _By Emily Nadler · 2026-03-06_ As a software agency, we regularly build products for clients serving specific populations, such as teachers, doctors, or children. Each has their own needs, and it’s best practice to design with those users in mind from the start. When a client asked us to build a financial literacy platform for users with intellectual disabilities, we realized that this population would force us to rethink our general assumptions about how people interact with technology more than other populations we’ve built for. We discovered that true accessibility is more than just checking compliance boxes, but instead about intentional design and creative feature choices. Many of those choices turned out to be more universally applicable than we’d expected. Here's some of what we learned. ## Text can be a barrier, not a feature One of the first things our client told us: "95% [of our participants] can't read. And if they can, it's very limited." This single insight changed our entire approach from how we would typically design a software product. We could no longer rely on labels, instructions, or error messages to guide users. Every interaction had to be visually intuitive. Images and icons had to carry meaning on their own, and text became supplementary, there primarily for the caregivers helping participants navigate the app. We learned to pair visual cues with minimal text, but the visuals always came first. One example: when a participant answers a question correctly, they are shown a confetti animation, along with the coin amount with emojis: ![](https://cms.echobind.com/assets/b871145e-8565-4357-b902-91dc05d94606) ## Designing for multiple users simultaneously We also learned that the users for our app would be using the app from centers where they would be accompanied by Direct Support Professionals (DSPs). These DSPs would sit beside them, helping them navigate the app interface and, in certain cases, answering questions on their behalf. This created a dual-interface challenge. Participants needed large buttons, visual feedback, and gamified rewards. However, DSPs needed monitoring tools, the ability to see multiple participants' progress at once, and administrative controls. Our solution was to build for both user needs, splitting the app out into multiple logins. The participant view was designed to be engaging, interactive, and accessible. The DSP view was designed to be informative, streamlined, and straightforward. For example, DSPs could answer questions on behalf of the participants, while the participants still got to see and interact with the questions, too. Overall, both users get the ideal user experience for their needs. ## Gamification as Curriculum For people with intellectual disabilities, the classroom experience isn’t just about information transfer, it’s about recognition. Our client described how their in-person financial literacy classes have typically functioned, before the creation of this app. When someone gets an answer right, the instructor celebrates publicly, everyone claps, and the participant feels seen. That moment of acknowledgement isn’t a nice-to-have. It’s what makes learning stick, which is why we needed to replicate that dopamine hit digitally. How we did it: - Coins for correct answers: Immediate, tangible rewards say “you did it.” - Badges for milestones: Allow participants to track their progress over the course of time - Visible progress bars: Clear visual indications of how far participants have come, and how close they are to their next achievement Research shows that gamification improves engagement and retention for learners with intellectual disabilities (i.e. [Kumar, 2025](https://restpublisher.com/wp-content/uploads/2025/03/The-Impact-of-Gamification-Strategies-on-Motivation-and-Knowledge-Retention-in-Learning-Environments-for-Persons-with-Intellectual-Disabilities.pdf)). Ideally, the visual progress indicators in our app would both reduce cognitive load around “how am I doing?” and the collectible elements would create intrinsic motivation to return to the app. Overall, our takeaway was that these features weren’t decorative, but instead primary features of our app. ## What Went Wrong Designing for multiple users simultaneously was a meaningful challenge, and we didn't get everything right the first time. We successfully designed the app such that a DSP could answer questions on behalf of a participant, which was an important accommodation for users who needed more hands-on support. What we missed, however, was the login experience itself. We hadn't accounted for the fact that a DSP might also need to log a participant into the app for them, not just assist them once they were already inside it. It was a gap that, in hindsight, revealed something important: truly understanding a user's workflow requires more than empathy in the abstract. It requires diligence at every step of the journey, especially for users whose day-to-day experience we don't have firsthand familiarity with. It's easy to design thoughtfully for the moments we can imagine clearly. The harder work is pressure-testing the full flow, including the parts that happen before a user ever reaches the first screen, and asking whether someone who relies on a caregiver for support can actually get there at all. ## What We're Taking Forward Building this app taught us that, when building for a certain population, accessibility is a lens through which every decision gets filtered. For example, we constantly found ourselves asking questions like: * "Will this work for someone who can't read?" * "Will this work for someone using it with a caregiver?" * "Will this make someone feel motivated to learn?" The answers to those questions shaped an app that is potentially simpler, more visual, more celebratory, and more human than anything we would have built by default. A lot of these principles would make any software better: fewer screens, clearer feedback, celebration of progress. These are more than accommodations for disability, they're good design. We just happened to learn them by building for a population that made the stakes impossible to ignore. --- [View on echobind.com](https://echobind.com/post/beyond-compliance) --- # How to Hire Stripe Developers > What to look for when you hire Stripe developers, from a team with more Stripe-certified developers and architects than any other agency in the US. _By Mike Cavaliere · 2025-08-19_ If you are searching for Stripe developers, you have probably already discovered the thing that makes this hire confusing: Stripe is genuinely easy to start with. The APIs are well designed, the docs are excellent, and any competent developer can have a test charge working in an afternoon. That is exactly why teams underestimate it. The hard part of a Stripe integration is not the first charge. It is the hundred edge cases between a working demo and a payment system you trust with real money — and those only become visible after you have shipped a lot of them. What you are really hiring for is not knowledge of the API. It is pattern recognition about what breaks. Here is what that difference looks like in practice, what Stripe certification actually verifies, and how to tell whether the team you are talking to has seen the problems you are about to hit. ## The part of Stripe that is actually hard Every integration starts simple. Then the business shows up with requirements. **Webhooks and fulfillment.** Stripe will deliver the same webhook more than once, out of order, and occasionally hours late. If your fulfillment logic is not idempotent, you will double-ship orders or double-grant access. This is the single most common defect we find in existing integrations, and it is usually invisible until volume exposes it. **Refunds, disputes, and partial states.** A payment is not a boolean. It can be authorized, captured, partially refunded, disputed, or reversed weeks later. Systems designed around "paid / not paid" have to be rebuilt once the first chargeback arrives. **Subscriptions and proration.** Upgrades mid-cycle, downgrades, pauses, trials that convert, seats that change on the fifteenth of the month. Stripe supports all of it, but the correct primitive is rarely the first one you reach for. **Money that moves between more than two parties.** The moment you have sellers, providers, or landlords receiving funds, you are in Stripe Connect, and you are making decisions about account types, payout timing, and dispute liability that are expensive to reverse later. **Migrations.** Moving off a legacy processor, or off Stripe's own deprecated APIs, means running two systems correctly at once while customers keep paying. None of these are exotic. They are the normal second act of a payments project, and they are where projects stall. ## What Stripe certification actually verifies "Stripe experience" on a résumé can mean someone copied a Checkout snippet once. Stripe's certification program is a real exam, and there are two distinct tracks that get conflated: **Stripe Certified Professional Developers** are full-stack engineers tested on hands-on implementation using Stripe-sanctioned best practices — the API surface, the security expectations, the correct handling of the flows above. **Stripe Certified Implementation Architects** are tested on something harder: choosing the right technical strategy for a business requirement, and leading developers through executing it. This is the person who tells you that your financing model should be built on Subscription Schedules rather than a Buy Now Pay Later integration, and can explain the tradeoff before you have written the code. Most teams need the second one earlier than they think. The costly mistakes in payments are architectural, not syntactic, and they get made in week one. Echobind maintains more Stripe-certified developers and Implementation Architects than any other agency in the United States, across both tracks. You can see the full breakdown on our [Stripe partner page](/partners/stripe). ## What volume actually buys you Certification proves someone knows the platform. Having shipped many integrations is what tells you which of Stripe's many correct-looking options is the right one for your situation. A few examples from our own work: **When the obvious Stripe product is the wrong one.** [CNY Fertility](/work/cny-fertility-stripe-billing-implementation) needed to offer patients financing for treatment. Stripe supports Buy Now Pay Later through Klarna and Affirm, but CNY wanted to provide their own financing, so neither was viable. We structured Stripe to collect the outstanding balance over exactly the term the patient selected at checkout. The right answer was a product built for a different purpose, applied sideways. That is not in the docs. **When the risk is in the change, not the code.** [PlayOn! Sports](/work/helping-play-on-sports-scale-their-stripe-payments) runs the largest high school sports ticketing platform in the US, processing over 300 transactions per second at peak on a Friday night. Their team was migrating from the legacy Charges API to Payment Intents ahead of a launch. They did not need us to write it — they needed two certified architects to spend two weeks auditing whether it would hold: correct Payment Intents flow, webhook handlers that fulfill reliably, security best practices, PCI compliance, and behavior at peak volume. Sometimes the highest-value engagement is a second set of certified eyes before you ship. **When multiple parties need to get paid.** [Dvora](/work/dvora-stripe-rental-property-payments) needed residents, landlords, property managers, and service providers all transacting inside one app — with scheduled rent, custom proration, configurable late fees, per-subscription control over automatic versus manual charges, and saved payment methods that work across different businesses. That is Connect, Elements, and Billing composed deliberately, designed as payment flow diagrams before any code was written. We are never without a Stripe project. That continuity is the actual product: we have already met the failure mode you are about to discover. ## When it makes sense to hire outside help Not every team needs to. It is worth being honest about when it does. ### You do not have an in-house engineering team If payments infrastructure is not something you want to own and maintain, hiring specialists lets you get the capability without building the org around it. ### Your team is capable but committed elsewhere Your engineers could learn Stripe deeply. The question is whether the months they would spend doing it are better spent on the product only they can build. Specialists are frequently the cheaper path once you price the opportunity cost. ### The specific thing you need is genuinely specialized Stripe Checkout is fast. Connect marketplaces, usage-based billing, multi-party payouts, and complex subscription models are not. Teams often start these confidently and reach a point where they realize a second opinion would have saved a rewrite. ### It has to be right the first time Payments is one of the few systems where a quiet bug is a financial and compliance problem rather than a UX annoyance. Getting the architecture reviewed before launch is materially cheaper than correcting it after money has moved. ### You have a team and want certified oversight Plenty of our engagements are advisory. Your engineers do the building; certified architects validate the approach, review the implementation, and keep the project out of corners that are expensive to back out of. If any of these sound like your situation, [tell us about your project](/contact). ## What to ask before you hire anyone Whoever you talk to, including us, these questions separate real experience from familiarity: - How do you make webhook handling idempotent, and what happens when Stripe delivers the same event twice? - Walk me through how you would model a mid-cycle plan change with proration. - When would you use Subscription Schedules instead of Subscriptions? - For a marketplace, how do you decide between Connect account types, and who carries dispute liability? - How would you migrate us off our current processor without downtime or double-charging? - What did you get wrong on a previous Stripe project, and how did you find it? Vague answers to these are the signal. Specific answers — ideally with a story attached — are what you are paying for. ## Working with Echobind We are a Stripe partner with certified Professional Developers and Implementation Architects on staff, and we have built and audited Stripe integrations across healthcare, ticketing, property management, and marketplace platforms. We take on full implementations, targeted architecture reviews, and advisory support for teams doing the work themselves. [Get in touch about your Stripe project](/contact), or read more about [our Stripe practice and credentials](/partners/stripe). --- [View on echobind.com](https://echobind.com/post/how-to-hire-stripe-developers) --- # GPT-5: Pros, Cons, and Why You Should Try It Today 🔧 > OpenAI’s GPT-5 has officially been released as of August 2025. Knowing when to and not to use it can save you money, time, and frustration. _By Mariah Grey · 2025-08-18_ As of August 2025, OpenAI’s GPT-5 has officially been released, and it’s already making waves. It has been noted as the most flexible and capable version of GPT thus far, but, as with every powerful tool, it’s not quite perfect. Knowing when NOT to use it can save you money, time, and frustration. ## The Pros (aka Why GPT-5 Is A BIG DEAL) ### Long-form context handling GPT-5 can manage more than 256,000 tokens at a time (that’s equivalent to hundreds of pages of text). This makes it a great option for use cases like in-depth analysis, multi-document processing, or long-form conversations. ### Improved Reasoning and Planning Problem-solving gets a major upgrade here, especially for multi-step processes. GPT-5 can break down your plans into steps and adapt as you go, which is awesome for coding projects (I’m looking at you, engineers), workflows, and preliminary research. ### Personalization In GPT-5, you can tailor the output to you. From the tone and how verbose responses are, to updating the “personality”, which allows answers to feel more authentic and aligned with your brand or style. ### Safety and Compliance Safety is a huge priority for many people, and GPT-5 understands that. It’s become more capable of responding with policy-compliant and safe outputs without stalling the conversation. This means it can tell you why things can’t be done a certain way and offers alternatives to keep you on track. ### Human-like Capabilities GPT-5 can handle multi-part jobs, such as data summarization or generating drafts, outlining all the steps without continuous prompts. All of these are significant improvements, especially the ability to handle large contexts, manage hundreds of pages of text, and offer comprehensive analysis. The improvement in reasoning and problem-solving is a clear benefit. Allowing technical users to hone the voice and complexity of the model makes it feel like their own, and prioritizing safety keeps users safe (maybe from themselves). Adding the ability to manage multi-part tasks and jobs without constant prompting makes GPT-5 a clear contender in the toolbox of users today. ## The Cons (aka Where GPT-5 Isn’t Quite There) ### Speed and Cost The advanced capabilities of GPT-5 mean that it takes longer and more resources to compute. If you’re using it for giant workloads, costs can climb quickly, and the response time can be significantly increased when compared to GPT-4o. ### Over Confident While GPT-5 is better at reasoning, it will still sometimes give wrong answers, in an extremely confident tone. This can be very dangerous for decision-making for users in a hurry who might not fact-check. ### Overcomplicated and Complex For quick question/answer sessions, GPT-5 could easily be overkill, while also being slower and more resource-heavy than smaller models. With additional customization options, there’s more to refine. Newer (or less technical) users might find it overwhelming compared to basic models. ### Mixed Feedback Some early users still strongly preferred GPT-4o for casual Q&A and creative writing, saying that GPT-5 felt “too technical” in some use cases. Even with the incredible advancements in GPT-5’s capabilities, it’s not the right option for every use case. - **Time-sensitive queries:** Faster (and cheaper) response times when using models like GPT-4o or smaller. - **Strict data isolation:** GPT-5 isn’t for you currently—it runs via cloud infrastructure, not local deployment. - **Financial, medical, or legal fields:** Fact-checking will continue to be critical as accuracy is still improving. - **High volume workloads:** If you’re hoping to use GPT-5 with a lot (read: millions) of requests, the costs will likely outweigh the benefits. - **Finely-tuned tasks:** GPT-5 is still too generally trained, rather than with specific models and datasets or targeted contexts. ## Bottom Line No matter what model you end up choosing, it’s hard to disagree with the simple fact that GPT-5 is the most versatile and advanced AI model offered to the public today. Its ability to handle complex, mult-step reasoning and large-context volume provides the brains and brawn to handle almost anything you can throw at it. Keep in mind, it’s not always about using the newest tool on the market, but the right one for your job, and GPT-5 just might be that tool. --- [View on echobind.com](https://echobind.com/post/gpt-5-pros-cons-and-why-you-should-try-it-today) --- # Understanding Generative AI: The Technology That's Reshaping Our Digital World > In the rapidly evolving landscape of artificial intelligence, **Generative AI** has captured the attention of technologists, creators, and businesses alike**.** But what exactly is generative AI, and why is it causing such a transformation across industries? _By Deloris Thompson · 2025-08-18_ In the rapidly evolving landscape of artificial intelligence, **Generative AI** has captured the attention of technologists, creators, and businesses alike**.** But what exactly is generative AI, and why is it causing such a transformation across industries? ## ## What is Generative AI? Generative AI creates new content, such as text, images, or other data types, in response to prompts. Such models have diverse applications across various industries, including content creation, software development, design, and beyond. At its core, generative AI is a model that accepts a **prompt** as input. The model then processes the prompt to output a response that is similar to the data it has already seen during training. ## ## The Role of Prompts **Prompts** are pieces of data that guide the model to complete its task. Prompts can include images, audio, code, or any other form of data that the model has been trained to understand. **Types of Prompts** * Zero-Shot * A Zero-shot prompt relies on existing knowledge. It is a single prompt that can contain instructions and additional content. * Few-Shot * Best suited for complex tasks, few-shot prompts provide a model with multiple examples. One example is a prompt that contains a JSON response format. * One-Shot * When a single example is provided to the model, it is considered a one-shot prompt. These work for predictable responses. ### **Models that Analyze (Discriminative Models)** Machine Learning or ML models are known as discriminative models because they discriminate between different types of inputs. They answer closed-ended questions, which have a limited, predefined set of answers. These models excel at classification tasks like: * Filtering spam * Identifying if a picture is a dog or a car, and more. ### ### **Models that Imagine (Generative AI)** In contrast, **generative models** guess what the data would be for a given prediction. These models utilize generative AI techniques in conjunction with other machine learning methods within specialized architectures. They can handle * Drafting an email * Replacing an object with another in an image * Creating a new 3D object for a design ### ### **Generative Adversarial Networks (GANs)** Introduced in 2014, one of the most fascinating approaches to generative AI is **Generative Adversarial Networks (GANs)**. This type of generative AI consists of two neural networks. A generative model and a discriminative model. They are trained together in a competitive relationship. They compete with one another, one trying to trick the other. After each round, they share insights, which improve both models over time. Think of GANs as two AI models locked in a creative competition: * **A generator model** attempts to create fake data that is indistinguishable from the real, trained data. * **A discriminator model** attempts to distinguish between real and fake data. ## ## The Evolution of AI: Recent Breakthroughs ### **Transformers: The Game Changers** The development of **transformers** has revolutionized the field of generative AI. These architectures provide several key advantages: * Understand the meaning and context within the text * Identify the relationships between words * Generate human-like responses that provide useful information * Allow models to handle multiple prompt elements at once ## ## The Impact and Future of Generative AI Generative AI represents a fundamental shift in how we interact with technology. Instead of rigid, programmed responses, we now have systems that can create, adapt, and generate content that feels remarkably human-like. As these technologies continue to evolve, we can expect to see increasingly sophisticated applications across various industries. From personalized content creation to complex problem-solving, generative AI is not just changing what's possible—it's redefining what it means to be creative in the digital age. The journey of generative AI has only just begun, and its potential to transform how we work, create, and communicate continues to unfold before our eyes. \*This blog post explores the fundamental concepts of generative AI, from basic definitions to advanced techniques like GANs and transformers. As this technology continues to evolve, staying informed about these developments will be crucial for anyone looking to understand and leverage AI in their work or personal projects.\* --- [View on echobind.com](https://echobind.com/post/understanding-generative-ai-the-technology-that-s-reshaping-our-digital-world) --- # Solving Nixpacks Node Version Issues with a custom nixpacks.toml > If you’ve tried upgrading your app to Node.js 22+ on a hosting service like [Railway](https://railway.app/) and hit walls despite doing *everything right*, you’re not alone. We recently ran into a frustrating issue where **Nixpacks** refused to use the correct Node.js version—even though we specified it everywhere. _By Deloris Thompson · 2025-08-18_ If you’ve tried upgrading your app to Node.js 22+ on a hosting service like [Railway](https://railway.app/) and hit walls despite doing *everything right*, you’re not alone. We recently ran into a frustrating issue where **Nixpacks** refused to use the correct Node.js version—even though we specified it everywhere. ## The Problem: Node Version Inconsistencies in Nixpacks We needed to deploy an app that required **Node.js \>= 22.12.0** for compatibility with [Directus 11](https://docs.directus.io). But when deploying to **staging or production**, the Railway builds kept **using a version that was one patch lower than needed**, breaking the app at runtime. We tried all the usual methods: ### What Didn't Work: * Setting `NIXPACKS_NODE_VERSION=22,`this ENV only allows setting major versions like 22 and not 22.12.0 * Adding `"engines": { "node": ">=22.12.0" }` in `package.json` * Adding an `.nvmrc` file with `22.12.0` Despite all of that, the builds would still pick up a **lower version**, causing failure. ## The Fix: Use `nixpacks.toml` with a `nixpkgsArchive` Pin The only reliable way to force the correct Node.js version was to **override the Nixpacks base archive** by creating a `nixpacks.toml` file: ```toml [phases.setup] nixpkgsArchive = '51ad838b03a05b1de6f9f2a0fffecee64a9788ee' ``` This commit hash from the [NixOS/nixpkgs repo](https://github.com/NixOS/nixpkgs) provides **Node.js 22.13.1**, which works perfectly with Directus 11\. The correct version was picked up, and our builds succeeded. ## Why This Works The `nixpkgsArchive` pin tells Nixpacks to use a **specific commit** of the Nix package repository, locking in the exact Node.js version available in that snapshot. Without this, hosting sites like Railway use a default nixpkgs archive—which may lag behind or skip specific Node versions. **Note:** This approach is similar to pinning dependencies in `package-lock.json`, but for your system-level environment. ## Finding the Right nixpkgs Commit for Your Version Want a different Node.js version in the future? You’ll need to find the right `nixpkgs` commit. Here’s how: 1. Go to the [nixpkgs GitHub repo](https://github.com/NixOS/nixpkgs). 2. Navigate to: `pkgs/development/web/nodejs/` 3. Check the `v22.nix` file across commits to find the one that introduced your desired version. ### Known Working Commits | Commit Hash | Node.js Version | | :---- | :---- | | `51ad838b03a05b1de6f9f2a0fffecee64a9788ee` | ✅ 22.13.1 (Confirmed working) | | `bf744fe90419885eefced41b3e5ae442d732712d` | ✅ 22+ versions available | | `ffeebf0acf3ae8b29f8c7049cd911b9636efd7e7` | ⚠️ 22.14.0 (from master, may be unstable) | For more info, check out [Nixpacks GitHub Issue \#1017](https://github.com/railwayapp/nixpacks/issues/1017), where this workaround is discussed. By using `nixpkgsArchive`, you get deterministic builds and avoid the frustrations of version mismatches, especially when platform requirements (like Directus needing Node \>=22) are strict. Let us know if you’ve hit this and found another workaround. Happy deploying\! # Resources * [Nixpacks Documentation](https://nixpacks.com/docs/configuration/file) --- [View on echobind.com](https://echobind.com/post/solving-nixpacks-node-version-issues-with-a-custom-nixpacks-toml) --- # Upgrading Directus from v10 to v11: A Safe and Reliable Guide > Upgrading from Directus 10 to 11 brings significant improvements and important changes that require planning. In this guide, we’ll walk through how to safely upgrade your Directus instance from version 10.5.3 to 11.10.0 without risking data loss or breaking your app. _By Deloris Thompson · 2025-08-18_ When I first learned I'd be upgrading Directus, I felt a familiar mix of confidence and apprehension. Having upgraded packages before, I knew the general process, but Directus is different. Schema changes, dropping outdated fields, or not typing them in are potential issues with CMS upgrades. As I dove into the upgrade process, I quickly realized this wasn't just another package update. The extra steps, like backing up the database first reminded me that production upgrades require a different mindset than development work. The local upgrade went smoothly, which gave me a false sense of security. Then came the deployment headache. Nixpacks refused to build with the Node.js version specified in our engine, and that's when the real challenge began. What started as a routine upgrade turned into a day-long puzzle that required creating a [custom nixpack file](https://docs.google.com/document/d/1HQ88BbIRYAzE-kDa75HdD9dmEXZd48idK4WbvEXEgag/edit?tab=t.0#heading=h.m7gs0cwmxhkh). By the end, I felt both exhausted and accomplished. The upgrade pushed me to understand deployment infrastructure in ways I hadn't before, turning what could have been a simple package update into a valuable learning experience about production deployment complexity. ## Important: Directus 11 Requires Node.js ≥ 22 Before starting the upgrade, make sure your local and deployment environments are running **Node.js version 22 or higher**. ### Check your Node version: ```bash node -v ``` If it's lower than `22`, upgrade Node before proceeding. We recommend using a version manager like [`nvm`](https://github.com/nvm-sh/nvm) or asdf: ```bash nvm install 22.12.0 nvm use 22.12.0 ``` ### Set Node version in `package.json` To ensure your deployment environment (e.g., Railway, Vercel, Netlify) uses the correct version, update your `package.json`: ```json "engines": { "node": ">=22.12.0" } ``` ### Watch for breaking changes in dependencies Upgrading Node may break other packages in your project. After switching Node versions, run: ```bash # NPM npm install # OR Yarn yarn install ``` And resolve any errors or dependency issues before proceeding. ### Example Error You Might See If Using Node \< 22 ```bash The engine "node" is incompatible with this module. Expected version ">=22.12.0". Got "18.20.4" error Found incompatible module. ``` ## Step 1: Review Breaking Changes Before upgrading, it’s critical to review **all breaking changes**, not just those in the most recent version. Review the [Directus breaking changes](https://directus.io/docs/releases/breaking-changes) to catch: * Field and API behavior changes * Role/permission updates * Any deprecated features or schema tweaks * Core system architecture changes * Extension/plugin compatibility notes * Auth and API adjustments Understanding what changed ensures you're not caught off guard after the upgrade. ## Step 2: Prepare a Local Testing Environment To avoid surprises in production, create a **local copy** of your staging (or production) environment. ### Clone the staging environment: * Export the current database * Copy your `.env` * Pull your current Docker setup (if applicable) ```bash # Example: Export DB pg_dump -U your_user -h staging-db-host your_db > staging_backup.sql ``` ## Step 3: Backup Everything Always take a backup of your **database** before any major upgrade. Whether you're working locally or pushing to staging, never upgrade without a clean rollback plan. ## Step 4: Upgrade the Directus Version Update your Directus package to version `11.10.0`. ### If using NPM/Yarn: ```bash # NPM npm install directus@11.10.0 # OR Yarn yarn add directus@11.10.0 ``` ### If using Docker: If you are self-hosting or using Docker for your build, this step is important. Update the `Dockerfile` or `docker-compose.yml` to use either V11.10.0 or the latest image: **v11.10.0:** ``` services: directus: image: directus/directus:11.10.0 ``` **Latest:** ``` services: directus: image: directus/directus:latest ``` Then rebuild: ```bash docker-compose build docker-compose up ``` ## Step 5: Run Migrations Locally After upgrading, run the Directus migration command **only in your local environment** to apply any necessary schema changes and confirm your data structure is still intact: ```bash npx directus migrate:latest ``` **Note:** On staging and production, migrations will run **automatically** during deployment, so no need to run this manually there. ## Step 6: Test Everything Locally Before deploying, verify your project works as expected: * Log in to the Admin Panel * Verify collections, fields, and relationships * Run common API queries * Test filters, roles, and permissions * Check custom endpoints or hooks * Confirm plugins or extensions still work This helps you catch breaking issues before they hit your users. ## Step 7: Deploy to Staging, Then Production Once everything checks out locally: 1. Deploy to your **staging** environment 2. Verify everything again in staging 3. Then deploy to **production** ## Optional: Clean Up After deployment: * Update your internal documentation * Remove deprecated config or unused fields * Ensure backup schedules are still intact ## You're Done\! Enjoy the performance improvements, new features, and better developer ergonomics\! Need help upgrading? Reach out and schedule a call today\! # Resources * [Directus Documentation](https://directus.io/docs/) * [Nixpacks Documentation](https://nixpacks.com/docs/configuration/file) --- [View on echobind.com](https://echobind.com/post/upgrading-directus-from-v10-to-v11-a-safe-and-reliable-guide) --- # How to Add Stripe Embedded Checkout to Rails 8 > Stripe's embedded checkout allows you to accept payments directly within your Rails app, eliminating the need to redirect users to external pages. This guide will help you to get it working with Rails 8 and Hotwire. _By Deloris Thompson · 2025-08-18_ Stripe's embedded checkout allows you to accept payments directly within your Rails app, eliminating the need to redirect users to external pages. This guide will help you to get it working with Rails 8 and Hotwire. ## What You'll Build A page that renders Stripe's embedded checkout form directly in your Rails app. That's it. No database, no user management, no webhooks \- just the core integration. ## Prerequisites - Rails 8 app with Hotwire - Stripe account (test mode or sandbox) ## Step 1: Set up Dependencies Add the following directly to your Gemfile and run bundle install: [Ruby Gems: Stripe](https://rubygems.org/gems/stripe) ```bash gem 'stripe', '~> 15.3' gem 'dotenv-rails', '~> 3.1', '>= 3.1.8' ``` Add the JavaScript SDK: ```bash NPM npm install @stripe/stripe-js yarn yarn add @stripe/stripe-js ``` ## Step 2: Configure Stripe Add the following to your **.env** file ``` STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here STRIPE_SECRET_KEY=sk_test_your_secret_key_here STRIPE_PRICE_ID=price_your_product_price_id_here ``` Create the initializer: ``` # config/initializers/stripe.rb Stripe.api_key = ENV["STRIPE_SECRET_KEY"] ``` ## Step 3: Create the Checkout Service This service class handles the Stripe API integration with clean, focused responsibility ```ruby # app/services/checkout_service.rb class CheckoutService def initialize(return_url:) @return_url = return_url end def create_checkout_session Stripe::Checkout::Session.create({ line_items: [{ price: ENV["STRIPE_PRICE_ID"], quantity: 1 }], mode: "payment", payment_method_types: ["card"], return_url: @return_url, ui_mode: "embedded" }) end end ``` ## Step 4: Create the Stimulus Controller & Register it Run the following command in your terminal to generate the controller and register it in the `app/javascript/controllers/index.js` file ```bash ./bin/rails generate stimulus stripeCheckout ``` ```javascript // app/javascript/controllers/stripe_checkout_controller.js import { Controller } from "@hotwired/stimulus" import { loadStripe } from '@stripe/stripe-js' export default class extends Controller { static values = { publishableKey: String, clientSecret: String } async connect() { const stripe = await loadStripe(this.publishableKeyValue) const checkout = await stripe.initEmbeddedCheckout({ clientSecret: this.clientSecretValue }); checkout.mount(this.element); } } ``` This Stimulus controller handles the frontend payment interface with minimal code: Configuration: The controller expects two data attributes from the Rails view: - publishableKey \- Your Stripe public key for client-side operations - clientSecret \- The checkout session secret from your Rails controller The **connect()** method runs automatically when the controller connects to a DOM element and: - Loads the Stripe JavaScript library using your publishable key - Initializes Stripe's embedded checkout component with the client secret - Mounts the complete checkout form directly into the HTML element ## Step 5: Update the Desired Controller This Rails controller handles the core functionality of a Stripe checkout flow in just two actions: The **show** action initiates the checkout process by: - Creating a new CheckoutService instance with a return URL for post-payment redirects - Generating a Stripe checkout session through the service - Extracting the client\_secret from the session to pass to the frontend for payment processing The **return** action handles the post-payment flow by: - Processing successful payments (this is where you'd typically update order status, send confirmation emails, etc.) - Redirecting users back to the homepage with a success message ```ruby # app/controllers/checkout_controller.rb class CheckoutController < ApplicationController def show checkout_service = CheckoutService.new(return_url: return_checkout_url) checkout_session = checkout_service.create_checkout_session @client_secret = checkout_session.client_secret end def return # Handle successful payment redirect_to root_path, notice: "Payment successful!" end end ``` ## Step 6: Set up Routes ```ruby # config/routes.rb Rails.application.routes.draw do root "checkout#show" resource :checkout, only: [:show] do get :return, on: :collection end end ``` ## Step 7: Create the View ```erb

Complete Your Purchase

``` Simple Stripe Checkout View This minimal Rails view creates a clean checkout page with just a few lines of HTML: **The Structure:** * Uses Tailwind CSS for styling with a centered container layout * Displays a "Complete Your Purchase" heading * Contains a div that connects to the Stimulus controller **The Magic Happens in the data-controller div:** * data-controller="stripe-checkout" connects to your Stimulus controller * data-stripe-checkout-publishable-key-value passes your Stripe public key from environment variables * data-stripe-checkout-client-secret-value passes the checkout session secret from your Rails controller **The Result:** When the page loads, the Stimulus controller automatically transforms the empty div into a complete Stripe checkout form with card fields, payment buttons, and error handling. **Pro Tip:** You can extract this into a partial for reusability: ## Testing 1. Start your server: `./bin/dev` 2. Visit `http://localhost:3000` 3. Test with card: `4242 4242 4242 4242` ## Don't Forget \- Additional Setup Required This guide gets Stripe embedded checkout working, but for a production application, you'll need: ### Required for Production - **Webhooks** \- Handle payment events (`checkout.session.completed`, `payment_intent.succeeded`) - **Database** **models** - Store orders, payments, and customer data - **User authentication** - Track who made purchases - **Error handling** - Handle Stripe API failures gracefully That's it\! You now have Stripe embedded checkout working in Rails 8\. The checkout form renders directly in your app without any redirects. At Echobind, we have experience working with Stripe and can help you integrate it in a new or exisiting application. --- [View on echobind.com](https://echobind.com/post/how-to-add-stripe-embedded-checkout-to-rails-8) --- # AI in Healthcare: Skip the Buzzword Bingo > "AI in Healthcare: Skip the Buzzword Bingo" cuts through the hype to show healthcare leaders how to start small, solve real problems, and see quick wins with AI. No jargon. No million-dollar price tags. Just practical steps to make healthcare a little less painful for staff and patients alike. _By Lex Meola · 2025-08-08_ ## AI in Healthcare: Skip the Buzzword Bingo You've heard about AI at conferences, board meetings, and probably your neighbor's barbecue. Congratulations—you're officially living in the future, where every conversation somehow circles back to ChatGPT. Here's the thing: AI doesn't require a PhD in computer science or your entire IT budget. You can start small without betting the farm. ## Why AI Sounds Impossibly Complicated Because everyone calls everything "AI." Machine learning finds patterns in patient data. Natural language processing reads medical notes faster than your residents. Automation handles the paperwork nobody wants to do. Different tools, same hype-laden umbrella term. No wonder it feels like drinking from a fire hose. ## AI You're Already Using (Without Realizing It) Your hospital probably runs AI that doesn't announce itself with fanfare: - Patient intake forms that actually make sense - Lab results that get flagged before someone has to manually review 847 normal cholesterol levels - No-show predictions that aren't based on reading tea leaves - Insurance claims that code themselves Boring? Maybe. Useful? Absolutely. ## Three Myths Keeping You Stuck **"It costs a fortune."** Small pilots cost less than your last consultant's PowerPoint deck. **"We'll fire everyone."** Unless your goal is replacing doctors with robots, AI mostly handles tasks humans hate doing anyway. **"We need to revolutionize everything overnight."** Or you could start with one annoying workflow and see what happens. ## How to Actually Get Started 1. Pick something genuinely annoying — prior authorizations, patient scheduling, whatever makes your staff contemplate career changes 2. Find a partner who knows HIPAA exists — surprisingly not everyone does 3. Run a short pilot — 6 weeks, not 6 months 4. Check if it actually worked — radical concept, we know ## The Real Point AI isn't about being trendy or impressing other executives. It's about making healthcare slightly less painful for everyone involved. Start small, stay practical, and ignore anyone who uses "paradigm shift" unironically. --- [View on echobind.com](https://echobind.com/post/ai-in-healthcare-simplified) --- # How to Set Up Stripe Terminal in React Native with Expo > Build a mobile payment app with Stripe Terminal and React Native using Expo, covering reader discovery, connection, and in-person payments on iOS. _By Preston Kelly · 2025-08-07_ In this guide, we'll walk through the high level steps you'll need to build a mobile payment app using [Stripe Terminal](https://stripe.com/terminal) and React Native. This guide will be the framework for a complete solution, you'll need to add your own UI, styling, error handling, etc. We'll focus on iOS, the principles are similar but not identical for Android. This guide uses [Expo](https://docs.expo.dev/) to simplify development. ## Why Stripe Terminal? Stripe Terminal provides a solution for accepting in-person payments using your device. It's ideal for: - Small businesses looking to accept card payments - Service providers who need to accept payments on the go - Developers building custom point-of-sale solutions - Retail stores requiring reliable payment processing ## Prerequisites Before we dive in, make sure you have: - iOS 16.7 or later - Xcode 14 or later - An Apple Developer account - A Stripe account - A compatible Bluetooth card reader - Node.js and Expo CLI installed (npm install -g expo-cli) - A physical iOS device ## Implementation Guide ### 1. Create a new Expo app Create an Expo app with the following command: ```bash npx create-expo-app@latest ``` ### 2. Setting Up Dependencies Let's install the necessary packages: ```bash npm install @stripe/stripe-terminal-react-native@0.0.1-beta.24 expo-build-properties@0.14.6 ``` ### 3. Configuring Your App Create or update your `app.config.ts` with the following configuration: ```typescript import type { ConfigContext, ExpoConfig } from 'expo/config'; export default ({ config }: ConfigContext): ExpoConfig => { return { name: 'stripe-terminal-app', slug: 'stripe-terminal-app', version: '1.0.0', orientation: 'portrait', icon: './assets/images/icon.png', userInterfaceStyle: 'automatic', ...config, ios: { ...config.ios, bundleIdentifier: 'com.yourcompany.stripe-terminal-app', }, plugins: [ ...(config.plugins || []), [ 'expo-build-properties', { ios: { deploymentTarget: '16.7', useFrameworks: 'static', }, }, ], [ '@stripe/stripe-terminal-react-native', { bluetoothBackgroundMode: true, locationWhenInUsePermission: 'Location access is required for payments.', }, ], ], }; }; ``` ### 4. Setting Up the Backend First, we need to set up a backend server to handle [connection tokens](https://docs.stripe.com/api/terminal/connection_tokens). Connection tokens are used by the Stripe SDK to connect to a reader. Create a simple [Express](https://expressjs.com/) server to handle connection tokens: ```typescript const express = require('express'); const stripe = require('stripe')('YOUR_STRIPE_SECRET_KEY'); const app = express(); app.use(express.json()); app.post('/connection_token', async (req, res) => { try { const token = await stripe.terminal.connectionTokens.create(); res.json({ secret: token.secret }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(3000, () => console.log('Server running on port 3000')); ``` ### 5. Creating the Terminal Provider Now that we have our backend server running, we can create a Terminal Provider that will use the connection token endpoint: ```typescript import { StripeTerminalProvider } from '@stripe/stripe-terminal-react-native'; import { ReactElement } from 'react'; const fetchTokenProvider = async () => { const response = await fetch('YOUR_BACKEND_URL/connection_token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }); const { secret } = await response.json(); return secret; }; export function TerminalProvider({ children }: { children: ReactElement }) { return ( {children} ); } ``` ### 6. Building the Payment Screen Create a payment screen that allows the user to discover readers, connect to a reader, and process a payment. For a more complete solution, you'll likely want a separate screen for reader management. For our high level example, we'll create a single screen that handles discovery, connection, and payment. Here are the essential code snippets: **Initialize Stripe Terminal:** ```typescript const { initialize, discoverReaders, connectReader, createPaymentIntent, collectPaymentMethod, confirmPaymentIntent } = useStripeTerminal(); useEffect(() => { initialize(); }, [initialize]); ``` **Discover Readers:** ```typescript const handleDiscoverReaders = async () => { await discoverReaders({ discoveryMethod: 'bluetoothScan' }); }; ``` > **Note:** The `useStripeTerminal` hook provides several callbacks, such as `onUpdateDiscoveredReaders`. You can use this callback to update your component state with the list of discovered readers, allowing you to display them in your UI and let the user select which reader to connect to. **Connect to a Reader:** ```typescript const handleConnectReader = async (reader: Reader.Type) => { await connectReader({ reader }, 'bluetoothScan'); }; ``` **Process a Payment:** ```typescript const handlePayment = async () => { // Create the payment intent const { error: intentError, paymentIntent } = await createPaymentIntent({ amount: 1000, // $10.00 currency: 'usd', captureMethod: 'automatic', }); if (intentError) return; // Collect the payment method const { error: collectError, paymentIntent: collectedIntent } = await collectPaymentMethod({ paymentIntent, updatePaymentIntent: true, }); if (collectError) return; // Confirm the payment await confirmPaymentIntent({ paymentIntent: collectedIntent }); }; ``` **UI Example:** ```typescript return ( ); }; export default ClaudeChat; ``` *[action.ts] The action that is called when the user submits a message. It streams the response from Claude and updates the UI with the response.* ```tsx "use server"; import { Anthropic } from "@anthropic-ai/sdk"; export async function streamMessage(msg: string) { try { const apiKey = process.env.ANTHROPIC_API_KEY; if (!apiKey) { throw new Error("Anthropic API key not found"); } const anthropic = new Anthropic({ apiKey, }); if (!msg) { throw new Error("Message is required"); } const stream = await anthropic.messages.create({ model: "claude-3-opus-20240229", max_tokens: 1024, messages: [{ role: "user", content: msg }], stream: true, }); let fullResponse = ""; for await (const chunk of stream) { if ( chunk.type === "content_block_delta" && chunk.delta.type === "text_delta" ) { fullResponse += chunk.delta.text; } } return fullResponse; } catch (error) { console.error("Error:", error); throw new Error("Error processing your request"); } } ``` ## Wrapping Up Integrating AI into your web applications doesn't have to be intimidating. Start small, experiment, and gradually expand your AI features. With the right approach, you'll be surprised at how quickly you can add powerful AI capabilities to your projects. Remember, the key is to start simple and build from there. Happy coding! --- [View on echobind.com](https://echobind.com/post/demystifying-ai-integrations) --- # Simplifying Stripe Connect with Embedded Components > How Stripe's embedded components ease the burden of implementing Custom Connect for a platform. _By Matt Thompson · 2024-09-09_ ## Introduction Implementing a robust payment system for platforms that connect businesses with customers can be a complex endeavor. Stripe Connect has long been a go-to solution for marketplaces, SaaS platforms, and crowdfunding sites. However, the implementation process often required significant development effort and careful attention to compliance requirements. At times even feeling locked in or insecure about decisions made between Express, Standard, and Custom Connect. Enter Stripe's embedded Connect components – a game-changer in simplifying this process. ## The Challenge of Custom Connect Traditionally, implementing Stripe Connect involved several challenges: 1. Building custom onboarding flows 2. Ensuring compliance with varying regulations across different countries 3. Maintaining and updating these systems as requirements change 4. Handling complex edge cases and user errors These challenges often led to longer development cycles and increased the potential for errors or oversights in the implementation. ## Stripe's Embedded Components: A Solution Stripe has introduced [components](https://docs.stripe.com/connect/get-started-connect-embedded-components) for Connect, which address many of these pain points. These pre-built, customizable UI components significantly reduce the complexity of implementing Connect. They are available as a plain JS SDK and React library to fit your needs. Here's how they help: ### 1. Simplified Onboarding The embedded components provide ready-to-use UI elements for onboarding connected accounts. These components handle: - Information collection - Identity verification - Bank account or debit card input By using these components, platforms can dramatically reduce the time and effort required to create a smooth onboarding experience. ### 2. Automatic Compliance Updates One of the biggest advantages of using Stripe's embedded components is that they automatically stay up-to-date with the latest regulatory requirements. This means: - Less worry about keeping up with changing regulations - Reduced risk of non-compliance - Fewer resources dedicated to maintaining compliance ### 3. Customization and Branding https://docs.stripe.com/connect/customize-connect-embedded-components Despite being pre-built, these components offer a high degree of customization. Platforms can: - Adjust the look and feel to match their brand - Choose which information to collect - Decide on the flow of the onboarding process This flexibility ensures that platforms can maintain their unique identity while benefiting from Stripe's robust infrastructure. ### 4. Improved User Experience The components are designed with user experience in mind. They offer: - Mobile-responsive designs - Clear error messaging - Intuitive flows that reduce user confusion This results in higher conversion rates for connected account onboarding. ## Implementation Example https://docs.stripe.com/stripe-js/react Here's a simple example from the [Stripe React Connect JS](https://github.com/stripe/react-connect-js) library on how you can leverage components for dashboards like Payment and Payout information. ```bash npm install --save @stripe/connect-js @stripe/react-connect-js ``` ------------ ```typescript import React from 'react'; import ReactDOM from 'react-dom'; import {loadConnectAndInitialize} from '@stripe/connect-js'; import { ConnectPayments, ConnectPayouts, ConnectPaymentDetails, ConnectComponentsProvider, } from '@stripe/react-connect-js'; const fetchClientSecret = async () => { // Fetch the AccountSession client secret by making an API call to your service }; const connectInstance = loadConnectAndInitialize({ publishableKey: '{{pk test123}}', fetchClientSecret: fetchClientSecret, appearance: { variables: { colorPrimary: '#228403', //optional appearance param, }, }, }); const App = () => ( { console.log('closed'); }} payment="pi_test123" /> ); ReactDOM.render(, document.body); ``` ## Resources For more information on Stripe Connect Components, check out the following resources: - [Stripe Connect Components](https://docs.stripe.com/connect/get-started-connect-embedded-components) - [Component List](https://docs.stripe.com/connect/supported-embedded-components) - [Stripe React Connect JS](https://github.com/stripe/react-connect-js) - [Stripe React Docs](https://docs.stripe.com/stripe-js/react) --- [View on echobind.com](https://echobind.com/post/simplifying-stripe-connect-with-embedded-components) --- # Browserless Puppeteer > Skip the PDF dependencies by leveraging Browserless with Puppeteer. _By Matt Thompson · 2024-09-04_ ## Intro During a recent migration of a service to Railway, we encountered a significant challenge with the Puppeteer package we were using for PDF generation. The package required numerous Linux dependencies, particularly for Linux-Chrome, which led to several issues: 1. Dramatically increased build times 2. Occasional build failures 3. Numerous warnings and troubleshooting logs, guided by Puppeteer's documentation: - [Puppeteer Linux Troubleshooting](https://pptr.dev/troubleshooting#chrome-doesnt-launch-on-linux) - [Puppeteer Chrome Troubleshooting](https://pptr.dev/troubleshooting#could-not-find-expected-browser-locally) Initially, we considered creating a custom Dockerfile to manage these dependencies, as suggested in this [Puppeteer/Chrome nixpacks example](https://github.com/ryannono/Puppeteer-Railway-Buildpack/blob/main/nixpacks.toml). However, our continued research led us to a more elegant solution: [Browserless](https://www.browserless.io/). We discovered that we could integrate Browserless within Railway to handle Puppeteer calls and manage all the necessary dependencies. This approach proved to be significantly simpler than building a custom Dockerfile, and the implementation process was surprisingly straightforward. ## The Migration The migration ended up being a pretty easy process. 1. Create a new Railway app and select the ["Browserless" template](https://railway.app/template/0jqemX). 2. Update the `package.json` to remove the `puppeteer` dependency and instead use the `puppeteer-ci` package. (This does not include the chromium browser dependencies) 3. Include the new `BROWSER_WS_ENDPOINT` environment variable in the Railway app. 4. Update the code to connect to the Browserless instance. (see below) ```typescript const getBrowser = async () => { if (process.env.BROWSER_WS_ENDPOINT) { // Use Browserless for staging and production return await puppeteer.connect({ browserWSEndpoint: process.env.BROWSER_WS_ENDPOINT, }); } else { // Fallback to local Chrome instance if BROWSER_WS_ENDPOINT is not set for local development return await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless=new'], ignoreDefaultArgs: ['--disable-extensions'], }); } }; const browser = await getBrowser(); // other logic here for generating PDFs const page = await browser.newPage(); // continue with PDF generation per normal ``` In the above code, we're checking for the `BROWSER_WS_ENDPOINT` environment variable to be set. With this set, we can now leverage the `connect` method of puppeteer to connect to the Browserless instance rather than launching a local browser instance. That was it! We were now able to leverage Browserless for PDF generation and not have to worry about any of the dependencies. ## Resources Browserless isn't limited to Puppeteer; it's a versatile solution that also supports [Playwright](https://playwright.dev/) and [Selenium](https://www.selenium.dev/). Consider exploring Browserless for your automation needs across these popular browser automation frameworks. - [Railway Browserless Template](https://railway.app/template/browserless) - [Browserless](https://github.com/railwayapp-templates/browserless) - [Browserless Docs](https://docs.browserless.io/#using-puppeteer) - [Puppeteer Docs](https://pptr.dev/) --- [View on echobind.com](https://echobind.com/post/browserless-puppeteer) --- # Oops, I accidentally made our website faster by switching to Remix > We migrated our site to a new hosting provider, and with it a new React framework. We expected the site to run a little slower. Our tests proved otherwise. _By Alex Anderson · 2024-09-03_ Echobind cares very much about using the right tools for the job, but we’re pragmatic enough to know when it’s worth while to migrate tools and services to save a couple of bucks. Recently, we’ve been feeling that way about the hosting for this very website, so we decided to say goodbye to our good friends at [Vercel](https://vercel.com/) and host our website on [Railway](https://railway.app/). We made this decision to make our monthly costs a little bit more predictable, and to bring more of our sites under the same hosting umbrella. This presented us with a small problem: Next.js. It [pairs wonderfully with Vercel](https://nextjs.org/docs/app/building-your-application/deploying#managed-nextjs-with-vercel), but requires [a bit more setup and infrastructure to run well when self-hosted](https://open-next.js.org/). We’ve been exploring alternative frameworks, and decided to see if [Remix](https://remix.run/) would serve our needs well. Given that it’s also a React framework, porting everything over should be fairly easy, right? ### Migrating from Next.js to Remix Moving the pages over from Next.js to Remix was actually easier than I expected. The Next.js site used App Router and React Server Components, but very few of those server components were nested. In most cases, I could copy the page code into Remix, replace the `` tags with the Remix equivalent, and move any data fetching into the Remix `loader` function. For the nested components, I changed them so their data was passed as props instead of fetched directly in the component body. That was the bulk of the migration. We also have a number of redirects [which Next.js handles out of the box](https://nextjs.org/docs/app/api-reference/next-config-js/redirects). In Remix, I had to create a function to process and handle those redirects for each request and run that function in the root route’s loader. ```javascript import { pathToRegexp } from "path-to-regexp"; const redirects = [ { source: pathToRegexp("/tagged/:tag"), destination: "/topic/:tag", permanent: true, }, { source: pathToRegexp("/blog-categories/:tag"), destination: "/topic/:tag", permanent: true, }, // ... ]; async function processRedirect(pathname: string) { const paths = await redirects(); for (const path of paths) { const match = path.source.exec(pathname); if (match) { // Match the params in the source to the params in the destination // This only supports a single URL param at this point. We could add more later const destination = path.destination.replace( /^\/.*\/(:.*)\/?$/g, (item: string, group: string) => item.replace(group, match[1]) ); // If we found a match, throw to tell Remix // to redirect to the correct page throw new Response(null, { status: path.permanent ? 301 : 302, headers: { Location: destination, }, }); } } } ``` Another Next.js convenience that isn’t built into Remix is the `` component, which helps keep page loads fast by correctly sizing and positioning images. Fortunately, [our CMS](https://directus.io/) has image resizing built in, so we could have roughly the same effect by putting `width` and `height` on regular `` tags and taking advantage of the CMS. ```javascript // Images for blog post cards, resized to the maximum necessary size {image?.description ``` You might need something a little bit more sophisticated than what we're using. Depending on what CDN you're using, you might be able to take advantage of its image transformation services. [Unpic](https://unpic.pics) provides a really nice API for automatically creating `` tags that take advantage of those image CDN transformations. Next.js App Router comes with automatic caching and static page generation which you specifically have to opt-out of on a page-by-page basis. This hopefully makes pages really fast, but adds extra burden to developers to make sure the cache is invalidated at the appropriate times, either with [Incremental Static Regeneration](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration) or [On-Demand Revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration#on-demand-revalidation-with-revalidatetag). On our old site, we opted for the latter with automatic triggers in our CMS that sent HTTP messages to our site whenever we changed a post or page. Remix, on the other hand, [relies on ](https://www.youtube.com/watch?v=bfLFHp7Sbkg)[`Cache-Control`](https://www.youtube.com/watch?v=bfLFHp7Sbkg)[ headers](https://www.youtube.com/watch?v=bfLFHp7Sbkg), which provides the same basic user experience without needing extra framework-level configuration. I just set a default `Cache-Control` header of `'public, max-age=300, s-maxage=3600, stale-while-revalidate'` on pretty much every page. Specifically, this says “Cache this in the browser for 5 minutes, and in a shared CDN cache for an hour. After that hour, the shared CDN cache will keep serving the stale cached content, but will re-fetch fresh content from the origin server. Hey, that’s just like ISR! One last convenience of Next.js is automatically generating and caching Open Graph images for link embeds. For that one, [I followed this guide by Jacob Paris and](https://www.jacobparis.com/content/remix-og) turned to the same tools Next.js uses internally: [Satori](https://github.com/vercel/satori), which converts JSX into an SVG image, and [resvg](https://github.com/RazrFalcon/resvg), which turns an SVG into a PNG. ```javascript import satori from "satori"; import { Resvg } from "@resvg/resvg-js"; export async function generatePng(jsx: JSX.Element) { const phantomFont = await fetch( new URL("https://echobind.com/fonts/PhantomSans0.8-Semibold.ttf"), ).then((res) => res.arrayBuffer()); const svg = await satori(jsx, { width: 1200, height: 630, fonts: [ { name: "Phantom Sans", // Use `fs` (Node.js only) or `fetch` to read the font as Buffer/ArrayBuffer and provide `data` here. data: phantomFont, weight: 300, style: "normal", }, ], }); const resvg = new Resvg(svg); const pngData = resvg.render(); return pngData.asPng(); } ``` To avoid needing to generate these image every time a link is embedded, I upload the image to our CMS after it's generated. If the image for a post is already in the CMS, it just serves that instead of generating it again. --- In short, it took a bit of extra effort to switch to Remix. And I thought the payoff would just be an easy time to host (it was incredibly easy to put this new site up on Railway). I wasn’t expecting any other kinds of improvements or performance gains, so when we ran some benchmarks, I was shocked by what we saw. ### The Results First off, Lighthouse. This is what our site looked like on Next.js: ![Lighthouse results showing 96 performance, 92 accessibility, 92 best practices, and 91 SEO](https://cms.echobind.com/assets/90f84db0-785c-4d9b-bb5b-9abd119f1a50) And this is what our site looked like on Remix: ![Lighthouse results showing 98 performance, 100 accessibility, 96 best practices, and 100 SEO](https://cms.echobind.com/assets/eedf9eea-e2bf-4383-8ba3-5b9cc36049a7) Again, that’s a Remix site hosted on Railway. Tough to say what exactly caused that 2 point performance improvement, but we won’t complain. Then we tested it on [ahrefs](https://ahrefs.com/), to make sure all of our links and SEO juice was still good. We were shocked to find our health score go up by 3% to 98%, our internal URLs with errors count go down by 94 to 11, and shockingly the number of oversized images dropped from 96 to 2. In other words, what started off as a migration turned into a performance optimization. And of course, this isn’t to say anything negative about Next.js or Vercel. They’re great tools and products built by great people. But switching our site to Remix and Railway paid off in spades. Not only that, but the Remix development experience feels more natural and straightforward to our developers, which makes building new features and maintaining our website all the more enjoyable. --- If you’re looking for a great framework, pick Remix. And if you’re looking for a great team to help you build (or migrate) your site or app with Remix, [pick Echobind](https://echobind.com/contact). --- [View on echobind.com](https://echobind.com/post/oops-i-accidentally-made-our-website-faster-by-switching-to-remix) --- # The MVP Approach to App Development > Start small with your app idea! Launch an MVP—a simple version to test and gather feedback. It’s like starting with a cheese pizza before the toppings! :pizza: _By Claire Surma · 2024-08-23_ So, you've got a groundbreaking app idea that's going to change the world. Before you spend your life savings building a digital behemoth, let's talk about starting with an MVP. No, we’re not talking about the Most Valuable Player here, although it can be a game-changer. An MVP in the software world stands for Minimum Viable Product—your app's very own cheese pizza. Let me explain... ### What the heck is an MVP? An MVP is the most simplified version of your app or website that highlights only your core features in a way that still delivers enough value to users. The goal of an MVP is to validate if people actually want what you are selling. It allows app owners to launch quickly, gather feedback, and iterate. It’s like a chef serving a dialed-in cheese pizza—no fancy toppings (yet!), but next-level perfection crust and a cheese-to-sauce ratio that will guarantee return patrons, especially among the most active pizza lovers. You get customers chowing down on your delicious cheese pizza, and then you sit back and listen to what they are saying. What do the restaurant-goers love? What do they hate? Slowly, you start incorporating new toppings based on the comments you hear from real customers. With each new pizza release, you get more folks stopping by, and in turn, more feedback. By letting the customers unknowingly direct the menu, you build both dedicated fans and a pizza you are confident in to be a crowd-pleaser. Launching your product as an MVP means being okay with imperfection at first. Your application doesn’t have to be feature-heavy and flawless from day one. It’s about being crystal clear on your core features and then learning, adapting, and evolving. And hey, it’s way more fun than stressing over every little detail of the never-before-served PB&J and hot sauce pizza before you even know if there’s a market. Sometimes, your MVP launch will be the least painful way to realize the market isn’t there. ### Why launch your new software idea as an MVP? 1. **Cost Efficiency and Risk Management** Building a full-featured product costs time and money—lots of it. There are technical challenges, market acceptance issues, and changing user preferences. An MVP approach helps you manage these risks by focusing first on the must-haves, cutting down on initial costs and allowing you to tackle potential problems early, without having to sink a ton of resources into a fully developed product. Once you know your product idea is a hit, you can invest more confidently in adding bells and whistles. 2. **Validate Market Need, Gather User Feedback, and Iterate** Why build a product no one wants? It also doesn’t matter how many features your product has if you have no users. An MVP is your ticket to validating market demand. By putting out a simplified version, you can gather real-world data and make sure you’re on the right track. Launching an MVP allows you to gather user feedback right from the start. This helps you understand what users are most attracted to, what they couldn’t care less about, and where you can improve. Iterating based on real user input means a better product that keeps your audience coming back. Focus on the best feature, get users, and then listen to your audience to prioritize and iterate! 3. **Clarity on the “Killer Features” and Core Value Proposition** An MVP forces you to hone in on your product’s core value proposition. By stripping away the non-essentials, you can focus on what truly sets your product apart and delivers the most value to users. This clarity is key to creating a strong product with a loyal user base. 4. **[Pressure Indicating UI](https://echobind.com/post/pressure-indicating-ui) for Non-Essential Features** Stripping non-essential features from your application’s MVP launch doesn’t mean your grand idea is all for naught; the non-essential features might come later after user behavior validates the need. One way to validate non-essential features is by including “pressure indicating UI tests” sprinkled throughout your MVP to confirm your assumptions before you pour time and money into them. Pressure indicating UI testing is a clever trick to validate what features resonate most with users. You create a clickable element on your site or app—let’s say, a bright blue button that says “Try Our New Feature!”—but the button doesn’t actually lead to the new feature. It’s a decoy, a test within a test. You can then track the clicks and get a direct measure of interest. Did a ton of users click the button? Great! That’s a strong indicator that there’s interest in your new feature. Was the button largely ignored? Maybe it’s time to rethink or refine the idea. You get genuine, unfiltered insights straight from real users who don’t know they are being tested, so their actions reflect true preferences and behaviors. This helps you prioritize development based on actual user demand. 5. **Scalability with Resource Optimization** Resources are often limited in the early stages of product development. An MVP approach ensures that these resources are used efficiently. Starting with an MVP gives you the flexibility to scale based on user feedback and market demands. By focusing on the essential data-driven priorities, you can allocate time, talent, and budget where they’ll have the greatest impact. As you gather insights and validate your hypotheses, you can make informed decisions about which features to develop next. This iterative process ensures that your product evolves in a direction that aligns with user needs, market trends, and your available resources. 6. **Gain Fans and Early Traction for Long-Term Success** Launching with an MVP helps you build momentum and generate buzz. Early adopters can become your biggest fans, spreading the word and attracting more users. This organic growth can provide invaluable marketing and help establish a loyal user base. Plus, early traction can attract potential investors and partners. By validating your product concept early, optimizing resources, and building a product that resonates with users, you pave the way for sustainable growth. An MVP isn’t the end game—it’s just the beginning of future version development and improvement. Launching your app as an MVP isn’t skimping out, it is a savvy strategy. By focusing on what’s essential and starting with the cheese pizza basics, listening to your customers, and gradually adding those irresistible toppings, you’ll not only save time and money but also save yourself the headache of launching a full product that majorly misses the mark with your audience. From speeding up your time to market and validating market need to gathering user feedback and managing risks, an MVP lets you build a product that’s both valuable and viable. Remember, the MVP is just the beginning! --- [View on echobind.com](https://echobind.com/post/the-mvp-approach-to-app-development) --- # Prisma Two Ways > Prisma has a default way to do migrations, where it manages everything from the schema. But there's nothing stopping you from migrating with raw SQL. _By Alex Anderson · 2024-07-03_ [Prisma](https://www.prisma.io/) client makes it incredibly easy to manage and query relational databases. I love how easy Prisma’s schema definition language makes modeling complicated relationships. ### Way 1: Schema → Migration → Tables For example, in this many-to-many relationship with multiple relationships, it’s pretty clear to see what’s going on, what’s required and what isn’t, and where the relationships all point: ```text model User { id String @id @default(cuid()) createdAt DateTime @default(now()) email String OrgUser OrgUser[] InvitedOrgUser OrgUser[] @relation("InvitedBy") } model Org { id String @id @default(cuid()) createdAt DateTime @default(now()) name String slug String @unique OrgUser OrgUser[] } // This is where org-specific user settings are stored model OrgUser { id String @id @default(cuid()) createdAt DateTime @default(now()) org Org @relation(fields: [orgId], references: [id]) user User? @relation(fields: [userId], references: [id]) invitedBy User? @relation("InvitedBy", fields: [invitedById], references: [id]) name String? role String orgId String userId String? invitedById String? } ``` And here’s that modeled as an ERD, courtesy [Prisma Editor](https://prisma-editor.vercel.app/). ![An ERD of the schema above](https://cms.echobind.com/assets/9631c7c0-79f7-432e-99e2-009aa869a641) After writing this schema, you can run `prisma migrate dev` and it will automatically create a migration with all the changes that you have made to your schema since the last migration, and apply it to your database. Handy! But for some, you might want to start with a SQL Migration and generate your tables from that. Prisma’s got a solution for that too! ### Way 2: Migration → Tables → Schema First, create a new migration in the Prisma migrations folder. Note that migrations are applied in alphanumerical order, so it’s a good idea to name your migration based on the current timestamp, like Prisma does: ```sql // /prisma/migrations/20240607153242_init/migration.sql CREATE TABLE "User" ( "id" TEXT NOT NULL PRIMARY KEY, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "email" TEXT NOT NULL ); CREATE TABLE "Org" ( "id" TEXT NOT NULL PRIMARY KEY, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "name" TEXT NOT NULL, "slug" TEXT NOT NULL ); CREATE TABLE "OrgUser" ( "id" TEXT NOT NULL PRIMARY KEY, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "name" TEXT, "role" TEXT NOT NULL, "orgId" TEXT NOT NULL, "userId" TEXT, "invitedById" TEXT, CONSTRAINT "OrgUser_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "Org" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT "OrgUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT "OrgUser_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE ); CREATE UNIQUE INDEX "Org_slug_key" ON "Org"("slug"); ``` Once you’ve done that, you can apply the migration to your database by running `prisma migrate deploy`. Now that our tables are updated, we need to get that schema back down to our app so Prisma can generate the type safe client for our app. Fortunately, doing that is just a single command: `prisma db pull` After running that command, the current structure of the database is put into your Prisma schema, nice and neat. ### Mix and Match And there’s nothing stopping you from doing both ways in the same project. Some folks might prefer to write the schema first. Others might prefer writing raw SQL migrations. If you’re careful, have good version control, and aren’t doing too many manual things in your Prisma schema, overwriting the schema with `prisma db pull` shouldn’t be too big of a deal. If it is, you can still run it, and then use source control to make sure you’re only changing the stuff that you want to. ### Way 3: Schema → Tables There’s another, forbidden way of using Prisma. I’m referring to `prisma db push`, which takes the current contents of your schema and puts it directly on your database. Yes, it gets the job done, but it’s heavy-handed. It doesn’t let you track changes or make subtle adjustments to tables before you alter them, like modifying column contents or moving data between columns. While it may sound tempting - why complicate things with an extra migrations step? - `prisma db push` runs the risk of accidentally overwriting or removing data. The Prisma docs have a lot more information about [when and why you might want to use ](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema#choosing-db-push-or-prisma-migrate)[`prisma db push`](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema#choosing-db-push-or-prisma-migrate)[.](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema#choosing-db-push-or-prisma-migrate) --- Personally, I prefer the balance of consistency, ease, and explicitness that comes from generating migrations from the schema. But maybe you really like writing SQL by hand. In either case, Prisma has options. --- [View on echobind.com](https://echobind.com/post/prisma-two-ways) --- # The House of Stripe > Echobind is a Stripe Services Implementation Specialized Partner in 2024 and that honors extends into our business strategy side, too. _By Zack Marty · 2024-06-10_ As a services implementation specialized partner with Stripe, we are often asked about whether our Stripe expertise extends into the business strategy side of the Stripe implementation. The short answer is yes, and if that is enough, then you can stop reading here, but I recommend that you keep going. When people first start thinking about Stripe it feels a whole lot like the below image. The money goes from the customer, through Stripe, and ends at your business. ![](https://cms.echobind.com/assets/0dec8c72-d526-491c-83c7-cd810e217995) It’s rarely this simple. Everyone would love for it to be this straightforward, but making sure that we build out the solution that aligns with your business goals and is executed strategically is of the utmost importance. The right strategy is similar to how plumbers think about building out the plumbing in a house. The way the average person thinks about plumbing is similar to the way that many think about Stripe. ![](https://cms.echobind.com/assets/7957b113-37d8-4b3d-b08c-ac7cea96c108) Though it may seem extremely simple, there are a number of variables that a plumber needs to consider when designing a good pipe system in a new home. Some of them are: - Based on the water supply, what type and size of pipe do I need to use to ensure that the residents have adequate water pressure? - How can I design the network of pipes such that water has the shortest distance to travel to and from each fixture? - Where do the pipes need the most insulation to protect from excessive heat and cold? - Where do I install pressure regulators to ensure that certain sections of the house have plenty of pressure while also guaranteeing that excess water pressure will not damage fixtures? - How do I work within the space constraints of a given house based on the structure of the house and the number of required fixtures? I could list many more considerations that a plumber will have through the house-building process, but this article is not about plumbing. It’s about how Echobind uses the right Stripe implementation strategy to help you reach your business goals. Let’s take a look at one of our clients who had some novel needs and how we planned out the strategy to ensure that we met each and every one of their business needs. ![](https://cms.echobind.com/assets/56aa2612-d5d6-40ae-9150-546620a385ca) ### Their needs Echobind’s client ran a platform that allowed users to donate to individuals and families who had fallen on hard times. Before they engaged with Echobind, they made every payment to the beneficiaries via paper check. However, many of these families could not wait for a check to arrive in the mail, so the client wanted donations and the disbursement of funds to move away from checks, especially since some checks never made it to their destination. Other than those needs, they had a few others: - They wanted site visitors to be able to tip the organization so that they could keep the site running. This meant that we needed to update the Stripe logic to make funds have multiple destinations. - They didn’t want any of the Stripe fees to be deducted from the funds that the beneficiaries received. To do this, we organized the flow of money such that Stripe fees came directly from tips, and the money donated would arrive to the beneficiary in full. - Oftentimes the campaign organizer and the end beneficiary were different people. This meant that we needed to create a way for beneficiaries to create a Stripe account that received funds from the campaign without them needing to be involved in the campaign. - People would sometimes create campaigns for non-profit organizations with whom they did not have an association, so they still needed the ability for money to run through Stripe while also being able to cut a check at the end of the campaign. This led to some exciting custom work that was outside of the bounds of Stripe’s boilerplate offerings. Echobind’s technical and product teams came up with a solution that allowed Echobind to exceed the client’s needs while also massively simplifying what the client thought would need to happen. ### Your needs Though that client’s needs were swiftly met, their needs are not your needs. So here are a few of the considerations we take into account when designing your Stripe implementation: - Who needs to interact with the system? - What platform will they use to interact with the system? - How will they interact with it? - Are you making an online marketplace, a place people log into in order to pay bills, or is your organization generating payment links for services and the customers receive those payment links via WhatsApp message? - What special regulatory considerations are needed in your industry? - Medicine is a far different beast than E-commerce, and we have done both. - What special considerations are important to your organization? - How much do we need to account for complexity vs simplicity on each side of a given transaction? In one instance we had a client who just needed to create a charge. The customer then needed to be able to dictate the pace at which they would pay the charge, allowing them to do full upfront payments or creating up to a five-year payment plan. This strategy significantly decreased defaults on payments by making them far more manageable for each customer. - Do you need to prorate payments, like an apartment complex, or are you creating a subscription service that starts on the day it was created? There is a whole lot more we think about when it comes to serving our client's strategic needs. Not only are we thinking about the Stripe of it all, but we also think about how the Stripe implementation will fit into your overarching product and business strategy. If you have any questions about how we can help you with Stripe, feel free to contact lex@echobind.com. --- [View on echobind.com](https://echobind.com/post/the-house-of-stripe) --- # How To Build An Event Ticketing System with Astro and Directus > Astro is excellent for content-driven websites, and pairing it with your favorite CMS creates the perfect combination. _By Dennis Campos · 2024-06-07_ [Astro](https://astro.build/) is excellent for content-driven websites, and pairing it with your favorite CMS creates the perfect combination. [Directus](https://directus.io/) offers a cloud-based service, Directus Cloud, as well as the option to run Directus locally. This guide will focus on setting it up locally, but you can also choose to set it up on the [cloud](https://directus.cloud/) if you prefer. We will create a super simple event ticketing system with the following model: ![](https://cms.echobind.com/assets/6551357e-b292-4ce9-83f8-e2fecda30c74) ## Prerequisites - Node.js ≥ 20 - Some knowledge of the Astro framework ## Set up Astro Run the following command to initialize an Astro project ```bash npm create astro@latest ``` Install Directus SDK ```bash npm install @directus/sdk directus ``` I'm using Tailwind, but feel free to use what you're comfortable with or even skip CSS entirely. ```bash npx astro add tailwind ``` Create a `.env` file. We will use SQLite, but you can choose the database you are most comfortable with. While Directus reads the `.env` file by default, you can choose to read a config file instead. For more options, please refer to the [documentation](https://docs.directus.io/self-hosted/config-options.html#configuration-options). This serves as a good starting point for our `.env` file. ```bash KEY="your_key" SECRET="superSecret" DB_CLIENT="sqlite3" DB_FILENAME="./sqlite.db" ADMIN_EMAIL="admin@echobind.com" ADMIN_PASSWORD="test1234" DIRECTUS_URL="http://localhost:8055" ``` If an admin email or password is not provided, Directus will supply default credentials, which will be displayed in the terminal. ## Set up Directus Once your db and your config file is set up, run the following command: ```bash npx directus bootstrap ``` Now you should be able to run Directus locally: ```bash npx directus start ``` Visit http://localhost:8055 and log in with your credentials specified in the `.env`. If you are unfamiliar with Directus, feel free to explore before continuing. ## Creating our models Refer to the model image displayed at the beginning of this article for assistance in creating the model and its fields. To create our models: - Click `settings` on the left side - Select data model - Create collections on the top right - Name it `events` and click next. I'm opting for the following fields provided by Directus: `date_created`, `date_updated`, and `status`. You may add or omit these options according to your needs. It won't affect the instructions in this guide. ![](https://cms.echobind.com/assets/1a8162d2-dfd8-4fc7-8503-ebb88a7d459d) Now that we have successfully created our model, let's proceed to create our fields. You can refer back to the model image to add the necessary fields. For illustration purposes, I will guide you through the creation of one field. The process will be similar for the remaining fields. - Create Field - Select Input - Under `key` input title and a type of `string` - Select `require value to be set on creation` - Save Now that you know how to add a field, please continue adding the remaining fields according to the model image provided earlier. Noting the `tickets` model, we will add a [many-to-one](https://docs.directus.io/app/data-model/relationships.html#many-to-one-m2o) relationship. This indicates that many tickets can belong to a single event. Here is how it should look. ![](https://cms.echobind.com/assets/3b11c729-3d6c-4de4-b1c0-815a2477a2df) ![](https://cms.echobind.com/assets/2a3c2009-c0e9-45bf-a2ae-a2c4dd4e1cd0) **Important -** To read, create, update and perform other tasks, we must provide access control. Otherwise, an error will occur in your application. ### Access Control - Go to settings - Access Control - Select Public - Click on the `read access` icon for the collections. It should change to a checkmark. In our app, we are not creating or updating anything because we are handling these tasks through Directus. However, feel free to do so if you plan on expanding beyond this guide. ## Set up types and Directus client Head back into our app and create a file name `directus.ts` under `src/lib` Let's define our types based on our image model. Here is what it should look like: ```tsx export type Event = { id: number; title: string; description: string; dateOfEvent: Date; location: string; }; export type Ticket = { id: number; eventId: Event["id"]; type: string; price: number; quantity: number; }; export type Schema = { events: Event[]; tickets: Ticket[]; }; ``` We will use `rest`. Alternatively, you can opt for `GraphQL` if you prefer that. ```tsx import { createDirectus, rest } from "@directus/sdk"; export const client = createDirectus(import.meta.env.DIRECTUS_URL).with( rest() ); ``` This is how the entire file should look. ```tsx // src/lib/directus.ts import { createDirectus, rest } from "@directus/sdk"; export type Event = { id: number; title: string; description: string; dateOfEvent: Date; location: string; }; export type Ticket = { id: number; eventId: Event["id"]; type: string; price: number; quantity: number; }; export type Schema = { events: Event[]; tickets: Ticket[]; }; export const client = createDirectus(import.meta.env.DIRECTUS_URL).with( rest() ); ``` ## Add data in Directus Head back to Directus and let's add some data. I had ChatGPT generate some events for me. Here is the JSON file in case you want to copy and paste http://jsonblob.com/1247570720585539584 or create your own! And here is the data for tickets: http://jsonblob.com/1247582973435174912 **Note:** You will want to attach the `eventId` to an event from your event data. The `ID` displayed in the link above is probably not going to match what you have. ## Display data in Astro It is time to display our added events from Directus. Head to our index file `src/pages/index.astro` and fetch our data using Directus sdk ```tsx // src/pages/index.astro --- import { readItems } from "@directus/sdk"; import { client } from "../lib/directus"; import { formatDateToLocale } from "../utils"; import Layout from "../layouts/Layout.astro"; const events = await client.request( readItems("events", { fields: ["id", "title", "description", "dateOfEvent", "location"], }) ); --- ``` Our markup ```tsx // src/pages/index.astro

Events

{ events.map((event) => (

{formatDateToLocale(event.dateOfEvent).month}

{formatDateToLocale(event.dateOfEvent).day}

{formatDateToLocale(event.dateOfEvent).time}

{event.title}

{event.description}

{event.location}

Learn More
)) }
``` In a separate terminal, launch the development server. Make sure the Directus server is also running. **Note:** The `formatDateToLocale` function above accepts a `Date` object and formats it into `month`, `day`, and `time` values as illustrated below. Feel free to create your own version. The outcome: ![](https://cms.echobind.com/assets/a6bbcd18-2592-4c27-8613-38c123dad4f9) ## Display a single event We can generate data based on an event using the parameters and Astro's `getStaticPaths()` function. More information is available [here](https://docs.astro.build/en/reference/api-reference/#getstaticpaths). Create a file name `index.astro` under `src/pages/event/[id]` and add the following: ```tsx // src/pages/event/[id]/index.astro --- import { readItems } from "@directus/sdk"; import { client, type Ticket } from "../../../lib/directus"; import { formatDateToLocale } from "../../../utils"; import Layout from "../../../layouts/Layout.astro"; export async function getStaticPaths() { const events = await client.request(readItems("events")); const tickets = await client.request(readItems("tickets")); return events.map((event) => { const eventTicket = tickets.find((ticket) => ticket.eventId === event.id); return { params: { id: event.id.toString(), }, props: { event, ticket: eventTicket as Ticket, }, }; }); } const { title, description, dateOfEvent, location } = Astro.props.event; const { type, price } = Astro.props.ticket; --- ``` Our markup ```tsx // src/pages/event/[id]

{title}

Description: {description}

Date and Time: {formatDateToLocale(dateOfEvent).day} {formatDateToLocale(dateOfEvent).month} {formatDateToLocale(dateOfEvent).time}

Location: {location}

Tickets

Type: {type ? type : "General Admission"}

Price: ${price ? price : "150"}

Purchase Ticket
``` Outcome ![](https://cms.echobind.com/assets/3738f1aa-549d-4352-8841-16485f5a40f8) ## Summary In this guide, we built a simple event ticketing system using Astro and Directus. With Directus set up, we fetched and displayed event data in Astro, and even created dynamic routes for individual event pages. This project highlights how Astro and Directus can work together to create a powerful, content-driven event management system. There are so many ways this can be improved and expanded. Whether you are building a simple event site or a full-featured application, this guide gives you the tools to get started and plenty of room to grow. *Dennis Campos is a software engineer here at Echobind. Want to work with us? Reach out at hi@echobind.com anytime.* --- [View on echobind.com](https://echobind.com/post/how-to-build-an-event-ticketing-system-with-astro-and-directus) --- # Dynamic OG Images > Do you ever want more control over your OG images for SEO purposes? Maybe you want to overlay some text dynamically? A blog title, perhaps. Read on to see how we implemented this inside our Rails app. _By Matt Thompson · 2024-06-05_ ## Dynamic OG Images Do you ever want more control over your [OG images](https://ogp.me/) for SEO purposes? Maybe you want to overlay some text dynamically? A blog title, perhaps. While there are many ways to accomplish this, continue on to see how I used [MiniMagik](https://github.com/minimagick/minimagick) within [Rails Carrierwave](https://github.com/carrierwaveuploader/carrierwave?tab=readme-ov-file#using-minimagick) to generate just that. ## Overview A quick application overview before we dive in. ```ruby # modles/post.rb class Post < ApplicationRecord mount_uploader :featured_image, FeaturedUploader ``` ```ruby # app/uploaders/featured_uploader.rb class FeaturedUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick storage :aws version :thumb do process resize_to_fit: [50, 50] end version :og do process resize_to_fill: [1200, 630] end # ...other options end ``` I have a standard Rails app with a `Post` Modal. That model has a `featured_image` column tied to Carrierwave through our FeaturedUploader class. In our current state, uploading an image to `featured_image` and saving the post will tell Carrierwave to generate two additional versions: a “thumbnail” that’s 50x50 and an “og” version that’s 1200x630 (recommended OG sizing). This is good on its own, but it only resizes our image. What if we wanted to do more? Insert MiniMagick options! ## Overlaying Text Our `Post` model also has a `title`. Let’s tap into that column for our text overlay. We will add a few more helpers inside our featured uploader and model. I’ll paste the code below, and then we’ll walk through what is happening. ```ruby # post.rb class Post < ApplicationRecord after_save :recreate_og_image def recreate_og_image return unless saved_change_to_title? || saved_change_to_featured_image? featured_image.recreate_versions!(:og) if featured_image.present? && title.present? end ``` ```ruby # app/uploaders/featured_uploader.rb version :og do process resize_to_fill: [1200, 630] # NEW LINE HERE process :add_text_overlay end private def add_text_overlay title = title_text manipulate! do |img| img.combine_options do |c| c.gravity 'Center' c.pointsize 50 c.draw "text 0,0 '#{title}'" c.fill 'white' end end end def title_text if model.present? && model.respond_to?(:title) model.title elsif title.present? title else 'Draft' end end def manipulate! cache_stored_file! unless cached? image = ::MiniMagick::Image.open(current_path) yield(image) image.write(current_path) end end ``` Let’s take this step-by-step. ### Step 1: Adding the `after_save` Callback in the Post Model In your `Post` model, add an `after_save` callback that triggers the recreation of image versions if the title has changed. ```ruby class Post < ApplicationRecord after_save :recreate_versions_if_title_changed private def recreate_versions_if_title_changed if saved_change_to_title? && featured_image.present? featured_image.recreate_versions!(:og) end end end ``` ### Step 2: Adding the `add_text_overlay` Process in FeaturedUploader In your `FeaturedUploader`, add the `add_text_overlay` process to the `og` version. When the OG version is recreated, this method will run again after resizing. Feel free to mix and match process methods per version. ```ruby version :og do process resize_to_fill: [1200, 630] # NEW LINE HERE process :add_text_overlay end ``` ### **Step 3 (The real logic)** add_text_overlay uses `manipulate!` from Carrierwave::MiniMagick to kick us off.. but there’s one catch. If I’m uploading the image from the form, I have the raw image file in hand. However, if I’m calling this from an after_save hook (title change), I’ll have the MiniMagik instance of this image with pointers to the file. To help with this, we’ve added a `manipulate!` method to override the previous one. In this method, we check to see if we have the cached temp file from our form first, and if not, leverage MiniMagick to open the file it has in hand. This ensures we have a raw image file before doing extra manipulations. Likewise, you’ll notice we do the same for the title, as depending on the trigger, the title may come from the `model` or straight as a field reference. ```ruby private def add_text_overlay title = title_text # add some sanitation helpers here for special characters... manipulate! do |img| img.combine_options do |c| c.gravity 'Center' c.pointsize 50 c.draw "text 0,0 '#{title}'" c.fill 'white' end end end def title_text if model.present? && model.respond_to?(:title) model.title elsif title.present? title else 'Draft' end end def manipulate! cache_stored_file! unless cached? image = ::MiniMagick::Image.open(current_path) yield(image) image.write(current_path) end ``` ### **The Options** [MiniMagick](https://github.com/minimagick/minimagick) has a ton of options, and this post barely scratches the surface. If you are diving in, I would encourage you to give their docs a good read. Let’s take a closer look at this section of our code. Here, you can see that we are tapping into options to perform a few actions. We draw some white text centered on the page, with a point size of 50. ```ruby img.combine_options do |c| c.gravity 'Center' c.pointsize 50 c.draw "text 0,0 '#{title}'" c.fill 'white' end ``` ## **Step 4 (Test it out!)** It’s Rails. Assuming you have scaffolded some CRUD routes, we can save the title and a featured image to see our result. Opening up our show view, we can add the line below to tap into our `og` version of the file saved and see the results. ```ruby # views/posts/show.html.erb ... <%= image_tag @post.featured_image.og.url %> ``` **And that’s it!** *Local Rails Demo* ![my-cool-og-post](https://cms.echobind.com/assets/e3da637c-7dc2-4c27-925d-fda718573795) ## Next Steps Tapping into MiniMagick more opens up endless possibilities, with multiple image overlays and more. ![my-cool-og-post-with-images](https://cms.echobind.com/assets/81be0e66-ef21-45ba-a784-ef17673a364e) 👋🏼 **Until next time!** --- [View on echobind.com](https://echobind.com/post/dynamic-og-images) --- # Roll your own CDN > What are CDNs, do you need one and how to create your own with S3 and CloudFront. _By Matt Thompson · 2024-06-05_ ## TL;DR In this article, we will cover CDNs, whether you need one, and how to create your own with S3 and CloudFront. If you're curious about any of this, read on! ## What are CDNs What is a CDN? A Content Delivery Network (CDN) is a system of distributed servers that deliver web content to users based on their geographic location. This results in faster load times and improved performance, as content is served from a server closer to the user. When it comes to CDNs for images, you've likely heard of major players like Cloudinary, Imgix, and ImageKit. These services excel with features like end-user cropping tools, extensive transformation options, and even new AI features to help enhance and edit. However, these benefits often come with significant costs and potential vendor lock-in. Additionally, features like external backups can quickly push you out of the free tier, making these services overkill for many use cases. So, do you even need a CDN? ## Evaluating CDNs Before setting up a CDN, consider whether you actually need one. For simple apps with a few static images—such as logos and accent images—hosting them alongside your app under `public/assets/` may suffice. With hosts like [Vercel](https://vercel.com/), placing images into a `public` folder will leverage their own CDN as the app is distributed. A CDN becomes essential when serving numerous dynamic assets to a global audience. For instance, blogs and e-commerce sites with images and videos for each post or item benefit greatly from a CDN, as it improves load times and SEO, especially for users far from your primary server location. As previously mentioned, all of this can come at a cost if you aren’t careful. Are there other solutions? Of course! It’s 2024, we have all sorts of options. ## Setting Up Your Own CDN with S3 and CloudFront If you decide a CDN is right for you but don't need the extensive features of services like Cloudinary, setting up Amazon S3 and CloudFront can be a cost-effective alternative. You can upload images to S3 in a way that suits your application, and once there, CloudFront can serve them globally without any issues. Below, we will go over step-by-step to see what it takes to roll your own CDN with AWS. To start, I’m going to assume you have the following: - An [AWS account](https://aws.amazon.com/) and basic working knowledge of the AWS console. - A domain you wish to use for your CDN. (optional, but recommended) - Basic understanding of DNS to add a CNAME. (we’ll walk through it 🙂) In our approach below with S3 and Cloudfront, you can create a base CDN for nearly free (outside our domain usage). Cloudfront / S3 costs are usage-based, and their free tier is quite extensive. [CloudFront Pricing](https://aws.amazon.com/cloudfront/pricing/) ![](https://cms.echobind.com/assets/edf53b5e-428e-4019-b1f3-ea3a2ea2e83c) ## Creating the Bucket First things first: We need a place to store our images. Insert good ‘ol AWS S3, which is known for becoming the junk drawer for most website assets. Let us head over to the AWS Console to S3 and create a new bucket. Feel free to name this bucket whatever you’d like, preferably something that makes sense for your application and/or environment. Example:`mysite-dev-assets` (local dev) `mysite-assets` (prod). When creating the bucket, you’ll want to leave everything as PRIVATE and block all public access. We’ll update the Bucket Policy later to include the permissions we need. ![](https://cms.echobind.com/assets/0cfb9a53-b50e-4baf-b533-c30256da05b5) While we are here, go ahead and upload an image to your bucket. Feel free to use any image you would like or our puppy friend below. Be sure to name the file something easy, as this will become part of our URL later. `puppy.jpeg` > `Screenshot Something something today's date.jpeg` ![](https://cms.echobind.com/assets/24ca854e-6289-47cd-a165-dd558cde1831) ## Creating the CDN with Cloudfront Alright, so now we have a bucket. Let's use CloudFront to create a CDN with our new bucket as the source. In AWS, search for CloudFront and navigate to its dashboard. Select “Create Distribution”. If this is your first time, you may see a marketing landing page from AWS instead of the normal dashboard - the link should be on the right side in their standard yellow as “get started”. You should be brought to a screen like the one below. Under Origin Domain, find your newly created bucket. Once selected, the name will auto-fill as well. I tend to leave this be and let it match, but feel free to update it to your liking (remember the name should you change it). ![](https://cms.echobind.com/assets/14b69631-331c-4dd6-8425-433832651e2f) The rest of the defaults should be good to start, but we’ll highlight a few to check here below. We’ll only need GET access from our CDN, and we’ll want to leave this as HTTP & HTTPS for now. **Protocol Access** ![](https://cms.echobind.com/assets/57ddc561-2854-4b0a-8ec3-a778961452d7) **Logs** One sneaky way AWS will hit your budget is with logs… unless you have a real need leave all the log options to “off” and “no” respectfully. ![](https://cms.echobind.com/assets/cee9f5de-a3d0-4edb-a7cf-5649e7814242) **Firewall WAF** This one is optional but worth pointing out. AWS offers some base protections for your CDN out of the box. You’ll note below that even at 0 requests, it’s a base $8.00 to add on. At something like 1 million requests, they estimate the WAF cost around $14.00. This seems like a nice addition, but it can be added anytime. We’ll skip this step for now, but feel free to enable it if you are okay with the base cost starting out. For our basic site, we’ll look at a free CDN (or close to it minus the domain). ![](https://cms.echobind.com/assets/52aa4fe6-2b02-4385-b8de-3ba1660c5b24) **Settings** Keep all the recommendations for Settings as well. Using all edge locations doesn’t really affect your cost—recalling the image above, it’s usage-based, meaning traffic comes through CloudFront regardless of location. Note the section for the **Custom SSL Certificate.** We’ll be back here later to update this. ![](https://cms.echobind.com/assets/eb2df9ae-b8b7-4f40-86f1-4dae28b3f2b5) **Click CREATE!** … spinning … spinning … AND Don’t close those toast warnings! ![](https://cms.echobind.com/assets/e119847f-31ac-49ab-ba78-38150a48d219) If we’ve done everything correctly, AWS tries to warn you that the Bucket Policy will need to be updated. You should see a Toast message with a “Copy Policy” button. If not, that’s ok. We can still take note of the `ARN` from the dashboard above to add our new bucket policy. If you copied the Bucket Policy, you should end up with something like the following. If not, take note of the `ARN` and your bucket name to stitch together the JSON below. While we are still on the CloudFront distribution page, copy the `Distribution domain name` somewhere for future use. This is our new CDN URL (internal use). **Sample Bucket Policy** ```json { "Version": "2008-10-17", "Id": "PolicyForCloudFrontPrivateContent", "Statement": [ { "Sid": "AllowCloudFrontServicePrincipal", "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::{YOUR_BUCKET_NAME}/*", "Condition": { "StringEquals": { "AWS:SourceArn": "{CLOUDFRONT_ARN}" } } }, ] } ``` ## Bucket Policy (CDN Permissions) Let us navigate back to S3 and our Bucket. Navigating to the permissions tab and scrolling down, you’ll see a white text box awaiting our new Bucket Policy. Let’s add the snippet above here and click save. This tells AWS that our CloudFront CDN has `GetObject` permissions for this specific bucket. i.e., it has read permissions for our future GET calls. ![](https://cms.echobind.com/assets/0dd779ef-591b-406e-8716-98414437468c) ## Testing our CDN Remember that `Distribution domain name`; let's give it a whirl. On its own, you’ll hit a nasty AWS permission XAML screen. We don’t have any `index` page set for this route as it’s not a full “site,” and that’s okay. Rather, you’ll want to match your buckets folder/asset structure and try to access your image. If you uploaded our `puppy.jpeg` image to your bucket earlier, your URL should look something like this. `your-domain.cloudFront.net/puppy.jpeg` If everything checks out, you should see our puppy friend hosted by your new CDN in your browser. If not, double-check the Bucket Policy and CloudFront status. Ensure everything is deployed, permissions are correct, and the URL/image you are trying to access exists in your bucket. 🎉🎉🎉 Take a moment to celebrate and grab a beverage. We have a CDN! 🎉🎉🎉 Next, we’ll add a domain and IAM permissions to finish getting this tied into something we can use in the real world. ## DNS, It’s always DNS Our CloudFront domain works, but I would not recommend showing the internal AWS domain to end users. It’s not the end of the world for CloudFront, but we try to avoid it as an AWS best practice in general. The more we can hide internals from end users, the better. We can use a subdomain to alias our CDN via a CNAME if you have a domain handy. `assets.mysite.com/puppy.jpeg` ### Adding a Subdomain Alias Navigate to where your domain is hosted, including Route53, DNSimple, GoDaddy, etc. We aim to add a single CNAME for our new CDN Subdomain. **CNAME:** `assets.mysite.com.` (*Note the end `.` may not be needed for some sites*) **Content/Value:** [`your-domain.cloudfront.net`](http://your-domain.cloudfront.net) (copy your CDN Domain here) **TTL:** Lower this to 1min or the smallest integer to kick everything off. Once set, we can bump this back up to the standard hour. **DNSSimple Example:** ![](https://cms.echobind.com/assets/f5bc4bba-3c87-4d20-900a-b7b79da8ce72) ### SSL Certificate CloudFront expects everything to be over HTTPS. For everything to work properly, we’ll need an SSL cert for our new subdomain tied to our instance. Head back over to the AWS console, but leave a tab open for more DNS changes. In AWS, search for a service called `Certificate Manager`. We’ll use this to generate our SSL certificate. Similar to before, if this is your first time, you may see the landing page. Look for a “Request Certificate” button, and we’ll dive in. 🚨The first thing you’ll want to check in the top right is that your AWS region is set to “N. Virginia ‘us-east-1’”. CloudFront has a requirement here for us-east-1. Once in ‘us-east-1’, we can continue with Request Certificate. Here, you’ll select `public` and land on the form for your SSL cert. You’ll only need to add your new subdomain to the `fully qualified domain name` list. *Note: if you want `www` as a part of this, add both here. (`assets.mysite.com` &[`www.assets.mysite.com`](http://www.assets.mysite.com))* ![](https://cms.echobind.com/assets/ac8eecb3-6615-4349-8994-cda97f7dab71) Leave DNS validation, the default algorithm selected, and any tags (if you prefer), then click request. Once created, you should see the start of the new SSL cert pending with a new CNAME name and value for your domain. *(You may need to refresh this page as it continues processing to see the updates)* ![](https://cms.echobind.com/assets/92b5dfa2-6777-4aac-95e6-8a874f771270) … and you guessed it. Navigate back to our DNS tab, and let's add that CNAME. Similarly, set the TTL here to something short until DNS has caught up and provisioned. *Note: If you already have an SSL Certificate from your domain provider for **the subdomain**, you can also use the `import` feature.* ### Updating the CDN Settings https://dnschecker.org/ We're almost there—DNS may take a bit to update. While we wait, let’s finish up our last step back in AWS. Under the CloudFront dashboard, let's select our distribution and click “Edit”. There are TWO settings we’ll need to update here. **Alternate Domain Name (CNAME)** This step is crucial for CloudFront to correctly associate the custom domain with your distribution and serve content over HTTPS using the appropriate SSL certificate. ![](https://cms.echobind.com/assets/bd001b6e-d503-44b8-8681-615c2a129f3b) **Adding the SSL Cert to CloudFront** Find and select your new SSL cert. ![](https://cms.echobind.com/assets/2c6bd7f6-0c27-45ca-a716-9e5b84fc18cd) **Click Save and wait for it to deploy…** ## Testing… Take 2 Once deployed, you should be able to see `puppy.jpeg` under your new CNAME alias. [`assets.mysite.com/puppy.jpeg`](http://assets.mysite.com/puppy.jpeg) ![puppy.jpeg](https://cms.echobind.com/assets/d5945592-e73e-4c41-987f-fe7f19f1428d) ## Applications In Practice We have a CDN 🎉… but how do we get images in it?! To get images into our source bucket, we’ll want to leverage an SDK with our app and utilize IAM keys for security. This could be any stack you choose, such as Node, Rails, C#, etc. The main change here is that we’ll need to update our Bucket Policy also to allow IAM keys for full CRUD operations. Depending on your stack, the SDK will guide you on using the IAM key and secret to perform S3 operations. I won’t try to cover all those options here. We’ll simply look to create the IAM role and update the bucket policy for any stack to utilize. Navigate to the AWS console one last time, and let's search for the `IAM` section. We will create a Programtic-only user and generate an API Key and Secret. Select the `Users` link on the left and `create a user`. Give this user a name specific to your application and environment for easy reference in the future (myapp-dev). Do NOT check the “access to console” option; our app will not need this. Next, click `attach policies directly` in the set permissions tab and search for “S3” in the new list. While you can refine which permissions you give these API keys, I’ll give it “[AmazonS3FullAccess](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-2#/policies/details/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonS3FullAccess)” to start (for demo’s sake). Continue to review and create; here, you can download and copy your key and value sets. 🚨Throw this in something like 1Password for future use - you will not see it again. Once created, you should land back on the user’s dashboard. Select your new user and take note of the `ARN` for that user. It should look something like this: `arn:aws:iam::{some-id}:user/{my-user-name}` Copy this somewhere, and let's head back to our S3 bucket to make one final Policy change. We’ll want to include BOTH the CloudFront permission and now our IAM permissions. ```json { "Version": "2008-10-17", "Id": "PolicyForCloudFrontPrivateContent", "Statement": [ { "Sid": "AllowCloudFrontServicePrincipal", "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::{YOUR_BUCKET_NAME}/*", "Condition": { "StringEquals": { "AWS:SourceArn": "{CLOUDFRONT_ARN}" } } }, // NEWLY ADDED PERMISSION SET FOR IAM ROLE { "Effect": "Allow", "Principal": { "AWS": "{IAM_USER_ARN}" }, "Action": "s3:*", "Resource": [ "arn:aws:s3:::{YOUR_BUCKET_NAME}", "arn:aws:s3:::{YOUR_BUCKET_NAME}/*" ] } ] } ``` ## Summary Congratulations. Now, when your application tries to upload or manage assets in this bucket, the IAM role has permission to do so. All while CloudFront and your domain are set to serve these assets as a CDN. We did it! Just remember, any time you want to use an asset — you’ll prefix it with your new [`assets.mysite.com`](http://assets.mysite.com) domain instead of the raw S3 path or internal CloudFront domain. ## Bonus: Image Variants As mentioned before, any stack/SDK can manage to upload images to your bucket. Multiple free solutions exist to convert larger images into different variations for application use. You could leverage the [Sharp](https://sharp.pixelplumbing.com/) or [ImageMagick](https://www.npmjs.com/package/imagemagick) kits in Node, for example. For Rails, you’ll likely leverage [Active Storage](https://guides.rubyonrails.org/active_storage_overview.html) or [Carrierwave](https://github.com/carrierwaveuploader/carrierwave), leveraging MiniMagick/Imagemagik in a similar way. The path here is up to you; the result is that you’ll have some async process to manipulate the raw image and leverage the S3 SDK to upload that new version to your source bucket. Below is a quick Rails / Carrierwave example: Here, we are leveraging MiniMagick to create a thumbnail and header variants. With Carrierave, we can also tack on our `asset_host` for a quick lookup later on when accessing this image from the model. We can create a quick util in our Model to nab the cdn_url on the fly. ```ruby # app/uploaders/image_uploader.rb class ImageUploader < CarrierWave::Uploader::Base include CarrierWave::MiniMagick storage :aws def store_dir # This would store in S3 as post/2/images/{versions}.jpeg "post/#{model.post_id}/#{mounted_as.to_s.pluralize}" end # Create different versions of your uploaded files: version :thumb do process resize_to_fit: [50, 50] end version :small_og do process resize_to_fill: [300, 157.5] end version :og do process resize_to_fill: [1200, 630] end def asset_host 'https://assets.mysite.com' end # Add an allowlist of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_allowlist %w[jpg jpeg gif png] end end ``` ------ ```ruby # A join table of Posts and Images class PostImage < ApplicationRecord ... mount_uploader :image, ImageUploader # psuedo code helper to get the cdn_url for thumb, og, or the raw image. # https://assets.mysite.com/post/3/thumb_puppy.jpeg def cdn_url(type = :raw_image) *#[:thumb, :og, :small_og]* version_keys = image.versions.keys uploaded_image = if version_keys.include?(type.to_sym) image.send(type) else image end "#{uploaded_image.asset_host}/#{uploaded_image.path}" end end ``` *Matt Thompson is a senior software engineer here at Echobind. Work with him and our talented engineers by reaching out to us at hi@echobind.com today.* --- [View on echobind.com](https://echobind.com/post/roll-your-own-cdn) --- # How To Make A Fantastic & Informative Flowchart > How to make a complex flow chart with easy-to-follow guidelines. _By Zack Marty · 2024-06-03_ For anyone who has ever needed to make a flowchart, they can seem like a daunting task, especially when they become harder to read as they become more complex. If you need to make a complex flow chart, below are some guidelines for how you can make them easier to read for your audience. These are not hard and fast rules, but they should generally be followed as well as possible. These rules do not necessarily apply to things like technical diagrams, but many of them can be used for technical diagrams that can get quite messy. ### **Rule 1. Multiple Arrows:** When multiple arrows originate from one shape, they should come from a decision point, which is typically indicated by a diamond AND when they come from a diamond, efforts should be made to make them come from multiple sides. If they have to come from one side, they need to come from the same point. This is the longest rule, so don’t worry, they’ll get shorter as we go. So let’s chat about these rules. The reason we create a decision point even when we don’t necessarily need one, is to prepare the viewer to know that we will be branching into different directions. You will have spots where something asks, “How do they log in?” and then you can proceed to show each of the ways in which a user will log in. The reason we have them come from multiple sides rather than the same side is that it makes it visually easier to come back to when you have multiple arrows, and it makes it easier to give yourself breathing room while making the flow chart. In the event that you need to make them come from the same side, then you need to make them come from the same point. Why? Because it looks messy and unprofessional if they are just slightly off from each other. ![](https://cms.echobind.com/assets/05d33cbd-d885-4a77-a32d-5bad169800dd) ### **Rule 2. Words on Arrows:** When words are entered on an arrow, they should be close enough to the point of differentiation that you don't need to move your eyes to see them. Oftentimes, when you have a point of differentiation that says something like, “Did the payment succeed?” followed by a yes and no scenario, you will place the Yes’s and No’s on the arrow instead of wasting space with a whole box. Any time that you need to do this, your Yes’s and No’s should be as close to the point of differentiation as possible, ESPECIALLY if you have more than two options. Like “What color did they pick?” Red, green, blue could be the options. Red means the bomb explodes, Green means the timer on the bomb in the action movie speeds up, and blue means you successfully diffused it. In this scenario, red, green, and blue on the arrows close to the point of differentiation makes it so that I don’t need to go searching for what choice was made along the flow chart. That being said, in the scenario of color choices, I might just make the arrows red, green, and blue. ![](https://cms.echobind.com/assets/a538af16-bd71-40f6-a592-589e7921d6d9) ### **Rule 3. Shape Assignment:** Tasks should always have a designated shape associated with them. As mentioned before, I use diamonds for decision points, and I also use this quadrilateral shape to indicate an action that someone took, like “I click on the submit button.” ![](https://cms.echobind.com/assets/8ca95999-d384-42d8-a8e8-1c5a9f19e93f) I tend to use this wide cylinder for any type of database entry, and I use rectangles for processes that happen, like “a timer appears on the screen.” ![](https://cms.echobind.com/assets/2504d9e1-73fb-4471-9987-7ff293cca33b) If you’re going to use a quadrilateral for user actions, then always use the quadrilateral for any user action, and if you want to designate different users performing an action, then you can use another color rather than using a separate shape. The other part of this is that you don’t want an event to be listed on an arrow. If I ask the question. “Did they type the correct password?” then **Yes** and **No** can be listed on the arrows as designators, but you wouldn’t want the next action taken to be listed on the arrow. That action, event, or task should get a shape. ![](https://cms.echobind.com/assets/258d61f2-79dc-4952-beb9-a31de7d01263) ### **Rule 4. Shape Rules:** The role that a given shape has should be consistent across a given diagram and the audience. To make information absorption as easy as possible, on a given flow chart, your shapes need to be consistent to the action or process they represent. Various blogs will claim that one shape should be used to indicate one thing or another, but ultimately, they just need to be consistent across a given diagram and the people to whom you show your flowcharts. At some point last year, I went from user actions being rectangles to making them quadrilateral. It’s relatively insignificant to pretty much everyone except for me, but if someone new is looking at one of your charts, then they should be able to distinguish between the various actions and processes at a glance. ![](https://cms.echobind.com/assets/188df5f0-4d6f-463d-9051-53c30f970c07) ### **Rule 5. Don’t cross lines:** If your lines cross, then you need to rearrange your diagram. This is simple. It’s better to have an arrow that takes a slightly wacky path than it is to have crossing lines. Crossing lines are hard to follow, and they create noise. When I use the term noise, I mean all of the extra junk information that does not lend directly to the information that you want to convey. Crossing lines are a distraction, and while some pieces of charting software have started to show a mini graphic that shows the line jumping over the other, it is still distracting and can create confusion where there shouldn’t be any. ![](https://cms.echobind.com/assets/7473b5b7-1f85-4659-b787-2e7164daf471) In the top left image, we are crossing lines, and it adds a distraction to the flow without adding valuable information. In the bottom left we have an overlapping line. Now it is hard to tell if the top left line is multi-directional or if information is going in only one direction. The reader can’t know without asking the creator of the chart. In the right chart we have separated that line so it doesn’t cross anything, and I will often use a dotted line to convey to the reader that we have a looping scenario. Each added piece of information should be deliberate, and no noise should be added unless that noise is also adding valuable information for the person who is viewing the chart. ![](https://cms.echobind.com/assets/41ecbf28-e59f-486f-9309-fb2accd08e2d) Even as we get into more complex charts we are able to follow all of these rules so that the diagram is easy to follow. None of these rules are hard and fast, but doing your best to follow them will make sharing and explaining flowcharts significantly easier for you and anyone else who may need to access it. If you run into any other issues when creating flow charts, feel free to email me with questions and keep an eye out for my next series of rules where we will dive into the finer details that will make your flow charts stand out in spectacular fashion. *Zack is a product strategist here at Echobind. Email him for tips, tricks and assistance at zack@echobind.com any time.* --- [View on echobind.com](https://echobind.com/post/how-to-make-a-fantastic-and-informative-flowchart) --- # Unlocking Success: The Benefits of Peer Feedback in Remote Tech Work > Prioritize personal growth at your agency to ensure happiness and overall success for yourself and your company. Here are tips to do so. _By Mariah Grey · 2024-05-24_ In the fast-paced environment of remote tech work, creating a culture of ongoing growth is among the most important for individual and company success. A great contributor to that improvement is peer feedback, giving benefits such as increased quality of work, creating an environment of collaboration, and ongoing development and learning. ## **Improving the Quality of Work** Feedback is a serious tool for improving the quality of work in remote tech environments. When teams come together and provide constructive criticism and share their insights, they grow together and can better identify and correct potential issues earlier in the development timeline. This leads to better quality deliverables and reduced errors. Differing perspectives can provide increased problem-solving approaches, as team members with different skills and experiences might see alternative solutions than initially considered. This not only improves work quality but also garners a sense of shared ownership for the final product. ## **Cultivating a Culture of Collaboration** Remote teams face the challenge of differing working styles and schedules. Peer feedback can address (and potentially alleviate some of) this by encouraging trust and open communication between colleagues. This empowers team members to share their thoughts and opinions without fear of judgment. Collaborative culture goes far beyond just team projects. It can lead to improved communication, and foster a sense of camaraderie among distributed teams, which in turn can prevent feelings of isolation. ## **Encouraging Ongoing Learning and Development** Being a remote worker requires ongoing learning to keep up with the ever-evolving tech and industry standards. Peer feedback allows team members to share their experiences and knowledge internally and allows others to collectively be on the cutting edge of the learning curve with the latest advancements and best practices. Receiving peer feedback gives people invaluable insight into where they have room for improvement as well as their strengths. This knowledge provides an opportunity for professional and personal development, giving team members options for skill enhancement and learning as needed. The entire team benefits by having an adaptable and skilled group. ## **Implementation** Implementing effective peer feedback techniques in remote tech work requires a thoughtful approach. Here are key steps to ensure its success: #### **Set Expectations** Have a clear outline for giving and receiving feedback. Communicate the reason for the feedback, and emphasize its purpose for personal or professional development. Encourage using a positive, constructive tone to create continuous improvement. This looks like: - Focus on the behavior, not the person: When giving feedback, be sure to focus on the specific behavior or action that needs improvement, rather than making personal attacks or criticism. - Be specific and objective: Provide specific examples of what was said or done that needs improvement, and how it impacted the team or project. Use objective language and avoid making assumptions or judgments. - Offer actionable solutions: Provide specific suggestions for how the person can improve and if possible, offer your support or other resources to help them implement the changes. - Be timely: Provide feedback as soon as possible after an event, so it remains fresh in everyone's mind. #### **Provide Training** Offer training or a simple how-to document covering effective feedback approaches. This provides team members with the needed skills to appropriately give actionable feedback. It can also emphasize the importance of empathy and tact when giving feedback remotely. #### **Use the Right Tools** Using tools that facilitate collaboration and communication is a sure way to encourage feedback. Platforms such as video calls, messaging applications, and management tools open the doors to dialogue and feedback. #### **Regular Cadence** Set a schedule for feedback. Different settings call for different schedules, so it may be monthly, weekly, or more often. Regardless of the interval, having a cadence and sticking to it will ensure that peer feedback is ongoing and a consistent piece of the team's workflow. --- Peer feedback in remote work is a powerful tool to improve work quality, foster collaboration, and promote ongoing development. By implementing clear expectations, training, using the right tools, and having a regular feedback cadence, remote tech teams can harness the full potential of peer feedback to achieve success 💫 *Mariah is a software engineer here at Echobind. Want to work with our incredible team? Reach out to us at [hi@echobind.com](mailto:hi@echobind.com) anytime.* --- [View on echobind.com](https://echobind.com/post/unlocking-success-the-benefits-of-peer-feedback-in-remote-tech-work) --- # Crafting Your App: 8 Discovery Steps for Decision-Making > Prioritize research and discovery exercises before you spend a lot of money to build your app. _By Eloisa Docton · 2024-05-22_ According to Google AI, the average app building cost is between $50,000 and $1.5M. Naturally, this depends on the type of app it is, the features needed, and the additional services that are required for the app to work. Money is definite, and we understand that. This is why planning for your app is important. The best way to plan for your app is by research and discovery exercises. With these exercises, we can discover opportunities, pitfalls, and—when necessary—pivoting to other solutions. ## Why discovery and research? Imagine inheriting a sizable sum from a long-lost wealthy relative. You have options: invest, pay debts, travel, educate your child, or, perhaps, finally develop that app you've long wanted. Take, for example, an app to help you focus on shopping goals at Target. While you and I might find it useful, do all Target shoppers share this need? Some view Target as a therapeutic escape; an app might disrupt this. Imagine investing your inheritance in the app, only to find limited usage or widespread dissatisfaction. To prevent this, research is key. It reveals diverse user needs, allowing you to tailor the app for success. ## How to perform your own discovery While experts can provide invaluable insights into your investment, there's no substitute for conducting your own discovery. At Echobind, [we offer numerous options to guide you](https://echobind.com/services), but starting with your own research is key. It provides clarity on your vision and accelerates your understanding of the problem you aim to solve. ![](https://cms.echobind.com/assets/630772de-eda6-4b98-8eb6-da50844d2659) ## DYI Discovery Step 1: Make a list **The Who:** Start by considering the obvious users—like you and me—but also think about whether everyone in your extended network, including family and friends, would use it. What about other shoppers you see at Target? Are there certain types of people who might not use it, like those who dislike using phones or despise shopping altogether? Ensure your plan accounts for these potential users; their data will be invaluable later. **The Why:** You likely have a good grasp of why you would use the app, but considering other potential users can uncover additional reasons for its utility. For instance, while you and I seek focus at Target, others may struggle elsewhere, like at Best Buy. Understanding these diverse needs can inform feature enhancements or potential pivots, revealing new opportunities. If half your users need it at Target and the other half prefer it for grocery or electronics shopping, you've struck gold. **The How:** Consider how users would interact with the app. While smartphones are the primary platform, think about compatibility with tablets or desktops. Not all devices have constant data connectivity, and not all apps work seamlessly on every phone or in areas with poor reception. Understanding these technical requirements is crucial, especially if users need the app in-store where signal strength may vary. Ensuring optimal functionality regardless of device or location is key to user satisfaction. ![](https://cms.echobind.com/assets/2eeba492-73b5-4b86-be65-4e402180f37a) ## DYI Discovery Step 2: Make sense of the list Now that you've gathered this data, it's time to define your problem and determine your solution. **1. Who would use it?** People who shop, not just at Target. Focusing solely on Target shoppers could overlook opportunities with those facing similar challenges elsewhere (as shown by our 50% example). **2. Why would people use it?** Because many tend to overspend at their favorite stores, and your app could offer budgeting assistance. **3. How would people use it?** Given that shoppers are often on the go, a mobile app would likely be the most practical solution. ![](https://cms.echobind.com/assets/8fed2c24-5fda-45a2-8109-f5f1a83e94c5) ## DYI Discovery Step 3: Digging deeper You've pinpointed who, why, and how the app will be used. Now, to ensure it caters effectively to all shoppers, aids in budget adherence, and is mobile-centric, we need more insight into these three aspects: - **Demographics**: Understanding the user demographics will inform visual design and language choices within the app. - **Socioeconomic status**: Grasping the financial circumstances of users will elucidate the true problem being addressed. - **Operating systems**: Identifying the predominant operating systems *(iOS or Android)* among users will guide technological decisions. Is the user base primarily iPhone or Android users? Would focusing on one OS alienate too many potential users? This knowledge is crucial in determining the appropriate technology to employ. ![](https://cms.echobind.com/assets/e94fb9c6-135e-4ac3-8954-7be405cd4d54) ## DYI Discovery Step 4: Tracing steps What are the steps users will take to use the app? 1. Get to the store 2. Fire up the app 3. See shopping list Wait ... how will this shopping list get there? Let’s try this again: 1. Create a shopping list 2. Get to the store 3. Fire up the app 4. See the shopping list 5. See the items in the list highlighted in the map Wait ... how will the app know what store you’re in? How will it know the store carries all of the products? Let’s try this one more time: 1. Create a shopping list 2. Connected to a store 3. Output any items not sold there, let you know 4. Get to the store 5. Make sure your phone knows your location 6. Fire up the app 7. See the shopping list 8. See the items in the list highlighted in the map Easily, going through this exercise a couple more times will provide even more discovery. We quickly went from 3 steps to 8, in the same exercise. ![](https://cms.echobind.com/assets/37593d43-e183-4fcf-9c68-83352e47110c) ## DYI Discovery Step 5: The Problem Now, with the insights we've gathered, the problem becomes clear: **The Problem**: People consistently overspend at their preferred stores, leading to detrimental effects on their budget. ![](https://cms.echobind.com/assets/6b752209-1ead-44ec-a87a-16295c35b9dd) ## DYI Discovery Step 6: The Answer An app that allows for you to enter your shopping list, match it with the items at the specific store, and maps the store out for you to stay on target ... not just at Target! ![](https://cms.echobind.com/assets/69e70b58-3bfb-49fd-8262-f2708b746091) ## DYI Discovery Step 7: Wait, does this already exists? Even if you haven't encountered a similar app, conducting a quick competitive analysis is crucial to ensure there aren't existing solutions addressing the same problem. In today's landscape, it's likely you'll find numerous similar apps. Therefore, it's essential to thoroughly assess each one, listing their features, comparing their strengths and weaknesses, evaluating their performance, reading reviews, and even testing them firsthand. This comprehensive analysis provides a deeper understanding of your competition, potentially inspiring additional features or services you may wish to offer, either immediately or in the future. ![](https://cms.echobind.com/assets/b465ac63-5d04-4db7-9356-c343362ff700) ## DYI Discovery Step 8: Most Lovable Product So far, we've identified who, how, and why people would use the app, and we've expanded its scope to include any store, not just Target. After examining competitors and validating ideas through research, it's time to assess our needs and budget. Here's what we believe is necessary: - A mobile app compatible with both Android and iOS - Shopping list storage capability - Access to store inventory - Store map integration - Directions to item locations within the store Considering these requirements, let's explore the technologies: - Building with React ensures compatibility with both operating systems. - Data storage is necessary for maintaining shopping lists. - Accessing store inventory would involve collaboration with stores and may introduce security complexities. - Store map integration may require reverse engineering existing solutions or exploring alternatives. - Locating products within the store could utilize similar methods as store map integration. Upon reflection, the need to connect to store inventory stands out. Do we truly require this? Could we find an alternative? Removing this requirement may impact the user experience, but let's reconsider: - Users can still visit the store and find available items without additional connections. - The only potential drawback is the need to visit another store to complete their shopping. With this reassessment, we can refine our app's requirements and better align them with our budget and goals. ![](https://cms.echobind.com/assets/f98e91eb-2e73-4145-b574-f91e4ef73149) ## So, what’s the product? We've made significant progress! Here's a summary of the essentials we've identified to ensure the app's usability: - A mobile app developed using React (a development code that allows for the same app to work on either phone without the need to build 2 apps) - Data storage functionality for saving shopping lists - Location services integration to determine the user's current store - Connection to the store's internal mapping system for item location Even without additional connections, users can still visit the store and locate available items. The only potential inconvenience is the possibility of needing to visit another store to complete their shopping. ![](https://cms.echobind.com/assets/cec7bf82-da5a-4053-84c6-2b18d20b1e66) ## Next Steps: Validation + Further Analysis Now that we've established a solid foundation for the idea, it's time to review whether additional research is necessary and transition into validation. Further research may involve examining existing store apps, such as Target's, to determine if they already offer features like saving shopping lists and displaying store maps. If they do, it might prompt us to consider another pivot. This leads us to validation. If everything seems aligned with the app idea, a second round of research could involve building a prototype and testing it with various potential users. By recording their impressions, we can gather valuable feedback to refine and improve the app. ## In the end, we are constantly researching Staying ahead of the curve is key. Even as the app is developed and utilized, it's crucial to maintain focus on the factors considered from the outset. Success hinges on continuous attention to users, competitors, and emerging trends. Alternatively, partnering with a company like Echobind can handle this for you, ensuring your app remains competitive and aligned with evolving market dynamics. *Eloisa is a senior designer at Echobind. Work with her and the rest of the team by simply emailing us at hi@echobind.com anytime.* --- [View on echobind.com](https://echobind.com/post/crafting-your-app-8-discovery-steps-for-decision-making) --- # Rails on Railway > A senior software engineer talks cost cutting and evaluating necessary recurring payments as a developer. _By Matt Thompson · 2024-05-13_ It’s no secret the year 2024 is one of re-evaluation and cost-cutting. As a Software Engineer, hosting the app is one of the unspoken costs that can chip away at us. For Rails specifically, I’ve seen these costs continue to grow. Today, we will explore ONE of the many options out there: [Railway](https://railway.app). I hope to continue this series with a few more options including Fly, Digital Ocean, and AWS. ## Past Resources As a Rails developer, [Heroku](https://www.heroku.com/) used to be the industry standard, and for many folks, it still is. However, in my opinion, Heroku got the Salesforce sentence of death. We eventually started to use [Render.com](http://Render.com). For the longest time, I was happy with Render... that is until we started hitting some crazy production costs. For a hobbyist app - Render is still great, but let's compare some of the costs for a “real” app. https://render.com/pricing Most production Rails applications historically have had a server, a database, and until recently, a worker of some kind with Sidekiq w/ Redis. Humble plug — Feel free to read more **[here on *“why you may not need Redis”*](https://echobind.com/post/you-may-not-need-redis-and-sidekiq)** in 2024. We can simply avoid that open-source drama altogether with something like [solid_queue](https://github.com/rails/solid_queue). With Render — anything past the “hobby” tier quickly grows to need something like Pro. That means we now have: - $20 per team member for access - $25/mo for a STANDARD Service (let's pretend the app is ok here..) - $85/mo for the worker - $95/mo for Postgres - $135/mo for Redis Let's see here… carry the one and… Just No. I couldn’t justify this—anyone who remembers the $5 droplet days would be shocked to see today’s market. Let's be honest: for some folks, there may be some justification for these costs, but try explaining this breakdown to a startup stakeholder. ![](https://cms.echobind.com/assets/1422a267-1503-4f01-803f-034baf658581) ## Railway Overview (TL;DR) One of the selling points of Railway is **usage-based** cost. I’ve already subtly mentioned one cost-cutting solution of **[removing Redis/Sidekiq](https://echobind.com/post/you-may-not-need-redis-and-sidekiq)**. Usage-based can be a double-edged sword here depending on your needs. There’s never a silver bullet solution. There will always be tradeoffs. However, consider our growing startups, internal apps, and personal hobby apps. With an internal example app below, I was able to take those drastic Render costs and cut costs to the estimated breakdown below. So far, for a small worker, Rails server, and Postgres, I’m sitting at ~$12 a month. I’ll take it. With some hosting services, I can’t even stand up Postgres for under $15. This gets a little closer to the OG $5 droplet days. If you are still in the exploration stages this is great. At some point, usage-based may not be the most cost-effective — but I’d imagine you are still better off here than with a tired price structure like Render. My current recommendation is that once usage-based becomes too much, you’re likely stable enough to start “enterprising” everything down in AWS/Azure. (more on that in a future post) ![](https://cms.echobind.com/assets/6862bf5d-e5f7-44ef-b9f8-f6618a2d3c0c) ## Running on Railway Railway has two options to deploy a rails app. Both include the standard recommended approach of linking your GitHub account/repo to a particular branch (defaulted to `main`). I’m not going to cover that piece here as it has become pretty standard practice for a lot of services today, and they do a pretty good job of walking you through it. Start by creating a new project and follow the GitHub prompts. Before the first deployment, you’ll have a chance to enter any Environment Variables. Take a second to add anything you’d need — leaving out the DATABASE_URL for now. This would include RAKE_ENV, RAILS_ENV, NODE_ENV, PORT, RAILS_MASTER_KEY, SECRET_KEY_BASE, etc. (depending on your setup) **With Docker** With Rails 7 you have a Dockerfile out of the box. If you make zero changes and link a `rails new` app to Railway, it will auto-detect the Dockerfile and build using Docker. This honestly just worked out of the box and was a great experience. However, things start to get a little tricky if you want to introduce that worker thread. Maybe it’s an old hat habit, but I tend to lean into the second option here. **With a Procfile** Good ‘ol Procfile. If you’ve deployed to Heroku and others in the past, this was the norm. Railway is also set to use a Procfile by default, so if you are migrating from Heroku or another, this should be straightforward. Railway even has some notes to cover that process in full [here](https://railway.app/heroku). For me and this sample app, I have the normal web app and solid_queue running in a side worker. ```jsx web: rake db:migrate && bin/rails server -b 0.0.0.0 -p ${PORT:-3000} worker: bundle exec rake solid_queue:start ``` For this to automatically work with Railway, I renamed the Dockerfile to something bogus and pushed it back up with my Procfile. Railway then uses its nix-packs and leverages the Procfile to kick everything off. You should see both the web and worker logs when viewing the “deploy logs.” **Adding the Database** At this point, your app may still be failing in the build due to not having a Database. Lets go ahead and take care of that. The first thing I update in the code is to mark my Production config to use `DATABASE_URL` we don’t really have control over the database name here and honestly, I’d rather manage one ENV > all the pieces. ```ruby # database.yml production: <<: *default url: <%= ENV["MY_APP_DATABASE_URL"] %> ``` Don’t push just yet! Back in Railway, let's add our PG Database. Render has a handy CMD+K / Ctrl+K command for adding a new service (or finding the button in the dash). A menu should pop up, allowing you to search for “Postgres.” ![](https://cms.echobind.com/assets/977c8acf-d230-401d-b280-f0dc01b91d27) Add the Postgres service and you should see both services running. ![](https://cms.echobind.com/assets/7bfd4a8d-6aac-4348-a1b6-5924406e4d60) Click on your repo application and click Variables. Railway has a quick helper to add the linked Postgres DATABASE_URL. This is super handy for ANY service you continue to add. ![](https://cms.echobind.com/assets/39968437-9ab2-4e3d-92f7-6d4cfd66cd0b) ![](https://cms.echobind.com/assets/b831a267-b228-4e15-a206-ec9deb92b27f) ### Re-Click Deploy! At this point, your new app should have all it needs to get rolling. From here monitor the build/deploy logs for anything specific to your application. ## Conclusion As I mentioned, there’s never a silver bullet solution for Software or deployment needs. That said, Railway does seem to offer some pretty solid Usage-based pricing that can take you pretty far. I’d highly consider having this as an option in your tool belt before buying into giant infrastructure (and the overhead that comes with it). Until next time! 🙂 *Matt is a senior software engineer at Echobind. Want to work with us on your next project? We'd love to help. Reach out to us at hi@echobind.com anytime.* --- [View on echobind.com](https://echobind.com/post/rails-on-railway) --- # You may not need Redis and Sidekiq > A senior software engineer talks about not needing Redis or Sidekiq anymore. _By Matt Thompson · 2024-05-13_ [In another post](https://echobind.com/post/rails-on-railway) I mentioned 2024 being the year of re-evaluation and cost cutting. It wasn’t long after starting some of these deployment evaluations that Redis caused a stir with its announcement of license changes. It’s always something, right? Most of my Redis interactions have been in Rails land, keeping globs of data in memory for quick access or the 1, 2 combo with Sidekiq. Add in the fact that a production Redis is stupidly expensive with services like Render. ($135/mo+ for the pro production version—ouch!) Accessing my own memory, I recalled some mention from last year's [Rails World](https://youtu.be/iqXjGiQ_D-A?si=9qt8EAWD0jNAU6Uz&t=2796) about leaning into the Hardware we already have. Memory access tools like Redis were invented/used when doing direct reads from an externally hosted database from a hard drive was slow (or slower). Today, we have Solid-State Drives, beefy cloud machines, shared data/edge resources, etc. To be clear, technology *has* actually advanced a bit; we have options. The Rails core community has been taking these thoughts and concepts to heart pushing and maintaining packages like [Solid Queue](https://github.com/rails/solid_queue) and [Solid Cache](https://github.com/rails/solid_cache). We are already approaching Rails World 2024, where tickets sold out in 20 minutes. While I’m not able to attend, I’m excited to see where the conversations take us. **Solid Cache** https://github.com/rails/solid_cache > Solid Cache is a database-backed Active Support cache store implementation. > > > Using SQL databases backed by SSDs we can have caches that are much larger and cheaper than traditional memory only Redis or Memcached backed caches. > Solid Cache is exactly what it sounds like. Why pay for an expensive in-memory Redis DB when your application is sitting inside an SSD, where reads and writes are crazy fast? Leveraging something like Postgres (which has also grown and scaled over the years) with SSD makes this a no-brainer for most use cases. **Solid Queue** https://github.com/rails/solid_queue > Solid Queue is a DB-based queuing backend for [Active Job](https://edgeguides.rubyonrails.org/active_job_basics.html), designed with simplicity and performance in mind. > Sorry, Sidekiq—you may no longer be needed either. In the same manner, we can leverage Postgres / SSDs to simply manage our jobs and processes. Even better, because this is DB-driven, we have direct insight into what is in the queue, what has failed, properties sent, etc. With a solid queue, you can even run this on the same thread as your application's thread via the Puma plugin. For example, I have an application with a small process that needs to run once per hour. This process doesn’t need a lot of resources; it’s not going to block the app and it completes in a few seconds. There’s no need for the added overhead of an additional worker thread — just tell the app to run the dang process. Again, even with a worker, comparing this to the overhead and costs of a full Sidekiq / Redis process is still a no-brainer for me. In a worst-case scenario, you beef up the box the Application/Postgres is hosted in. That is still a huge cost-saving difference from adding full in-memory services like Redis at scale. While there are alternatives to solid_queue like [good_job](https://github.com/bensheldon/good_job), both solid_cache and solid_queue are becoming an increased part of the Rails core packages, maintained under the Rails org. In my opinion, there’s no real need to look elsewhere, as these are built to directly interact out of the box with Active Job. ## Summary I don’t like to lay down blanket statements, especially in the Software industry. However, the use cases I once had for Sidekiq and Redis are closing. Simply put, do your homework. Don’t pay for services you don’t need. Until next time! 🙂 *Matt is a senior software engineer at Echobind. Want to work with us on your next project? We'd love to help. Reach out to us at hi@echobind.com anytime.* --- [View on echobind.com](https://echobind.com/post/you-may-not-need-redis-and-sidekiq) --- # Leveraging Stripe To Manage Your SaaS Entitlements > This is how to use Stripe to manage your SaaS Entitlements. _By Kishan Gajera · 2024-05-08_ If you have a software-as-a-service product, you likely have multiple subscription plans with different feature sets for your customers to choose from. For example, our [Labs](https://echobind.com/post/2024-the-year-of-products-at-echobind) project, [Kind Kiosk](https://kindkiosk.com/), has the following subscription plans: ![](https://cms.echobind.com/assets/393b736e-398e-4f70-8e71-1e78a6811d96) Until recently, your back-end implementation was responsible for managing entitlements for a customer’s subscribed plan. It can get complex to keep your back-end in sync with the customer’s subscription throughout its lifecycle as recurring payments may fail or the plan is upgraded, downgraded, or canceled. Amongst many of the larger features announced at [Sessions](https://stripesessions.com/), Stripe quietly released a new [Entitlements API](https://docs.stripe.com/billing/entitlements). This API lets you create features which can then be assigned to products. As of writing this post, features can only be created using Stripe’s API and not through the Stripe Dashboard. Continuing to use the example plans above, you’ll have the corresponding products in Stripe: ![](https://cms.echobind.com/assets/b70de9f7-4ea7-48fa-9e11-9b645959c0b7) For customers subscribed to our “Growth” plan, they are allowed to create an unlimited amount of designations or causes. So let’s create a feature for this and assign it to our “Growth” product: ```tsx const feature = await stripe.entitlements.features.create({ name: 'Unlimited Causes/Designations', lookup_key: 'unlimited_designations', }); // Assign the newly created feature to a product const growthProductFeature = await stripe.products.createFeature( 'prod_growth', { entitlement_feature: feature.id, } ); ``` You would repeat the above for all the features needed for your subscription plans. Now that all your features have been added to products, how can we use these to know which features a customer has access to? Stripe maintains a list of active entitlements for a customer when they subscribe to a plan and also keeps them up to date throughout the different events that may occur in the subscription lifecycle. Your app can be notified of changes to a customer’s entitlements by using [Stripe Webhooks](https://docs.stripe.com/webhooks). Whenever the customer’s active entitlements change (for example, when they subscribe, change, or cancel a plan), the `entitlements.active_entitlement_summary.updated` webhook event will be sent. Here’s an example of the data your webhook endpoint will receive: ```json { "object": { "object": "entitlements.active_entitlement_summary", "customer": "cus_MgWgA7lAKUOkm1", "entitlements": { "object": "list", "data": [ { "id": "ent_test_61QL65z2ila6DkHsA41DvCN66DEZt1Z2", "object": "entitlements.active_entitlement", "feature": "feat_test_61QL64Bts1fTy44Im41DvCN66DEZtA3s", "livemode": false, "lookup_key": "unlimited_designations" } ], "has_more": false, "url": "/v1/customer/cus_MgWgA7lAKUOkm1/entitlements" }, "livemode": false }, "previous_attributes": { "entitlements": { "data": [] } } } ``` You’ll likely need to retrieve a customer’s entitlements frequently, so Stripe recommends persisting the active entitlements in your database for performance reasons rather than fetching them on demand using the [list API](https://docs.stripe.com/api/entitlements/active-entitlement/list). These can be persisted when processing the webhook event. If you’re using [Postgres](https://www.postgresql.org/), it could be as simple as having a `user_id` column and an `entitlements` column which is an array of the `lookup_key` value from Stripe’s active entitlement object: ![](https://cms.echobind.com/assets/51fd567b-5521-4e57-9760-1acf3cca448f) Now throughout your codebase, you can quickly retrieve your user’s entitlements and implement access control to features of your product. By placing the burden on Stripe to maintain entitlements, that should simplify your implementation and keep you focused on implementing your actual product. *Kishan is a senior software engineer at Echobind; want to work with him on a project? Write us an email at hi@echobind.com and we'll be in touch this week.* --- [View on echobind.com](https://echobind.com/post/leveraging-stripe-to-manage-your-saa-s-entitlements) --- # Become Safe to Disagree With > This blog is for professionals who don’t feel safe disagreeing with others, to make sure your voice is heard in a safe way at your office, remote or in-person. _By Zack Marty · 2024-05-06_ A treasured colleague recently told me, “I feel safe disagreeing with you.” I was touched insofar as someone told me that they felt safe with me, but what did it actually mean, how could I go about making sure that everyone felt that way with me, and how can I help foster an environment where more people feel safe disagreeing with their colleagues? For those of us who don’t feel safe disagreeing with others, this blog is about helping you find a way to make sure your voice is heard in a safe way. For those who have no problem disagreeing with others, this blog is about how you can foster an environment that cherishes and recognizes healthy conflict. ### What is healthy conflict? In short, healthy conflict is just a [constructive disagreement](https://www.linkedin.com/pulse/healthy-vs-unhealthy-conflict-workplace-thiru-damodharan-mba/). It’s when people are able to disagree with each other, and each party listens intently to understand and value the alternate positions that people present. Such conflict in the workplace breeds creativity, personal development, and stronger bonds with your workmates. It’s not always easy to pull off, and it is something that you need to foster within yourself as well as others. Healthy conflict requires a [strong ego](https://midsonshort.medium.com/the-difference-between-a-big-ego-and-a-strong-ego-3f103746dc44) from everyone involved. Having a strong ego means that you can like and appreciate your own intellect while also understanding that others will have better ideas, and you need to internalize that disagreements are not a personal attack on your intellect. Sometimes your perspective comes from being in the weeds and theirs is from a more visionary standpoint. Sometimes their opinion comes from outside of the industry as a way to shake things up and yours comes from industry expertise. Either way, you likely both have good reasons to hold the opinions you have, and recognizing that will lead to healthier and more productive disagreements. This took a lot of practice, and quite a few hours of self-reflection after I failed to disagree in a healthy and safe way on more than one occasion. ### How can I foster an environment of healthy conflict? It's unrealistic to expect perfect agreement with colleagues every time. We all have off days where our responses may be less professional and our communication less effective than intended. When such situations arise, it's important to humble ourselves and offer apologies when warranted. Personally, I've found myself needing to apologize to my colleague Claire on more than one occasion. While no one explicitly required me to do so, failing to apologize would likely strain our relationship, which is detrimental both personally and to the company as a whole. However, constantly finding oneself in a cycle of apologies isn't conducive to a healthy work environment. Therefore, as leaders and colleagues, it's vital to cultivate trust by honoring the [commitments we make to one another](https://hbr.org/2018/01/why-we-should-be-disagreeing-more-at-work). These commitments are rarely spoken and are more often implied through our actions and general behavior. You can tell everyone who will listen that you can be trusted and that you value people’s input, but that doesn’t matter one iota if you routinely demonstrate that you are hostile toward critical input, never address your own shortcomings, and routinely break people’s trust. This all comes down to the simple idea that you need to treat people the way that you want to be treated. ![](https://cms.echobind.com/assets/e15a3f2d-f932-41b9-8950-11e97b15cc72) ### So how do I actually start? Like this: - Listen to criticism actively and respond thoughtfully instead of waiting to speak. Engage with the feedback rather than just collecting it to defend your own opinion. - If you disagree with someone, jot down their points and reflect on them later. Emotions can cloud judgment, so taking time to process can reveal valid insights or the value in their perspective. - If you've already considered and rejected an idea, explain why it doesn't work rather than dismissing it outright. This fosters constructive dialogue and understanding. - Seek further clarification when needed. Asking for more detail demonstrates active listening and respect for others' perspectives. Make sure to do this often, especially when you understand 98% of what they said. That last 2% can make all the difference. This is as lovely advice when you’re a leader/boss at your place of work or play, but how do we go about fostering a workplace of healthy conflict when we’re not the boss? And how do you make yourself heard? ### Being Heard The difficult truth is that you need to lead if you have not been given a platform to be heard. If this can’t be done where you are, then you might be in the wrong place, but let’s assume you’re somewhere with people willing to hear you. You can lead without being the boss, and fostering healthy conflict when you aren’t the boss is all about leading by example. There are numerous ways to do this [without wielding power](https://hbr.org/2013/05/how-to-lead-when-youre-not-in). This can be done by connecting people to each other at your workplace, being a curious contrarian, or asking a lot of questions. The way I have been most effective at this is by peeling back the layers of decisions to understand the roots. When you are able to peel back layers, one method being the [five why’s](https://www.mindtools.com/a3mi00v/5-whys), people will notice your curiosity and investigative nature. Though we are all supposed to care about our work, passion can shine through. ### Getting Started In a meeting, attempting to pick apart the intricacies of a decision, especially if it's your first time doing so, can often prove more disruptive than beneficial. Try to initiate this process with a single individual, preferably someone with greater decision-making authority or influence than yourself, whether through their official position or their sway among peers. Despite everyone's crazy schedules, most people are willing to set aside time for a one-on-one discussion with a colleague. Once you've got their time, begin unpacking the decision, asking probing questions, and getting into the specifics. Over time, as you engage in these discussions, they will naturally begin seeking your input. This gradual approach alleviates the pressure of suddenly voicing opinions in a meeting, which can be daunting for those inexperienced in such situations. Your consistent presence and contributions will become expected and valued by those you've engaged with individually, leading them to actively involve you rather than requiring you to assert yourself. ### Conclusion None of us are perfect, and not everyone wants to jump into the limelight. However, we all want to be heard, and we all want to hear from others. You can become easy to disagree with, and you can make it easier to disagree with others in a healthy way by starting with individuals and expanding from there. You will have to put yourself out there, and you need a strong sense of self to do it, but it gets a whole lot easier the more you do it. *Zack is a product strategist here at Echobind and would love to help you plan out the roadmap of your next big project. Let’s do this together; email us at [hi@echobind.com](mailto:hi@echobind.com) anytime.* --- [View on echobind.com](https://echobind.com/post/become-safe-to-disagree-with) --- # Our Takeaways from Stripe Sessions 2024 in San Francisco > Stripe Sessions 2024 was an unbelievable event in San Francisco for Echobind. Here are our immediate takeaways from attending. _By Kishan Gajera · 2024-05-01_ Our team is back from [Stripe Sessions](https://stripesessions.com/) and we are excited to share all the new updates. The Stripe team covered a lot of ground and we can’t wait to help our clients take advantage of all the new features that were released. ## New Features Announced There were too many features announced at Sessions to cover in this post but we’ve highlighted a few features below that will have immediate positive impacts for our clients as well as our [Labs](https://echobind.com/post/2024-the-year-of-products-at-echobind) projects. In general, a pattern we noticed throughout a majority of the features announced were: - they can be directly configured through the Stripe Dashboard or using low-code solutions - they allow imposing your business logic onto the Stripe platform Both of these points result in you writing and maintaining less code and getting your product out there faster. ### Stripe Connect Embeddable Components Stripe now has [17 embeddable components](https://docs.stripe.com/connect/get-started-connect-embedded-components) that can be used with [Stripe Connect](https://stripe.com/connect). This has greatly reduced the complexity and development time needed to build Connect implementations. With this low-code solution, it allows you to embed different pieces of Stripe functionality directly into your app: ![](https://cms.echobind.com/assets/23ecdf48-3cce-4faf-a07e-7eb755a51adf) In our past implementations, we have found the [Stripe Hosted Onboarding](https://docs.stripe.com/connect/custom/hosted-onboarding) flow to provide the most friction and confusing experience for end-users so the component we’ll focus on migrating to in many of our projects is the [Account Onboarding component](https://docs.stripe.com/connect/supported-embedded-components/account-onboarding). We’ve already started improving the onboarding experience for our Labs product, [Kind Kiosk](https://kindkiosk.com/). In the screenshot below, you can see the differences in UI between the two approaches. On the left, we have the hosted onboarding flow, where a user has to click a button to be redirected away from our app to a Stripe hosted page. On the right, we have embedded the account onboarding component customized to use our Kind Kiosk brand colors which provides a much more seamless flow to our users. ![](https://cms.echobind.com/assets/02365fe7-b7b3-44bd-a644-a7d962bdeb89) ### Stripe Sigma AI Assistant Many of our clients have been leveraging [Stripe Sigma](https://stripe.com/sigma) to create reports to pull various data they need out of Stripe. In order to use Sigma, you need to know how to write a SQL query, which likely means a developer would need to get involved. It’s now possible to use natural language to generate the SQL query which breaks down the barrier for accountants and other roles to use Sigma. We are looking forward to introducing this to our clients: ![](https://cms.echobind.com/assets/186a57dd-c465-4cc0-92b5-4ed46b2095f0) ### Payment Methods Stripe has continuously been making improvements to increase conversions for their [Checkout](https://stripe.com/payments/checkout) suite and they’ve released a number of enhancements around payment method presentment. Stripe has also added more payment methods options such as [Amazon Pay](https://pay.amazon.com/) and 50+ others that can be easily enabled through the Stripe dashboard. With the number of payment method options increasing, it can be difficult to know which ones are appropriate to present to your customer. We learned that Stripe now takes on the burden of this with [Dynamic Payment Methods](https://docs.stripe.com/payments/payment-methods/dynamic-payment-methods) which uses their AI models trained on millions of businesses to help present the right payment methods to user based on their location, currency, device type, browser, transaction amount, and other factors. In addition to this, Stripe announced additional options for managing presentment by configuring [payment rules](https://docs.stripe.com/payments/payment-method-rules) or setting up [experiments for A/B testing](https://docs.stripe.com/payments/a-b-testing) through the Stripe dashboard: ![](https://cms.echobind.com/assets/3ce987e1-536c-4dff-9085-c9b7f8937e70) ## Echobind’s Stripe Partnership We’ve been [partners](https://stripe.partners/) with Stripe since the start of their partnership program and we are proud to announce that we've been [officially recognized as a Stripe Services Implementation Specialized Partner](https://stripe.partners/directory/echobind) in 2024, the highest achievement in the Stripe Partner Ecosystem. ![](https://cms.echobind.com/assets/fe464594-fd57-4726-bd38-09bacc991969) The Stripe Services Implementation Specialization is awarded to partners who have demonstrated excellence in leading successful implementations, driving user activations, and passing Stripe's capabilities assessment, showcased by their proficiency in delivering high-quality services to customers. We've worked hard [to learn the intricacies of Stripe's software](https://echobind.com/partners/stripe) and align it with our client's needs to create the most seamless, effective marketplaces imaginable. This indicates that you're partnering with a proven services provider, skilled in Stripe implementation and dedicated to your success. In short? We want to give you the most professional (and effective) experience possible. We're excited about the opportunity to partner with you to showcase our Stripe skillset and how quickly we can push your company forward. *Kishan is a senior software engineer here at Echobind. Want his expertise on your next big project? Shoot us a note at hi@echobind.com and we'll chat all about it.* --- [View on echobind.com](https://echobind.com/post/takeaways-from-stripe-sessions-2024) --- # Project Management Tools and Adapting to Client Workflows > How to customize your project management strategies to sync with your clients' workflows effectively. _By Claire Surma · 2024-04-30_ ## Introduction At Echobind, we believe that understanding and adapting to the unique needs of each client is not just an approach but a necessity. This blog post delves into how we customize our project management strategies to sync with our clients' workflows, tools, and overall business ethos, ensuring our exceptional projects are matched with high-level processes. ## **Customizing Project Management to Client Needs** From startups to established enterprises, the diversity in workflow and technology stack is vast and varied. To navigate this diversity, Echobind does not just apply a standardized template of management practices; instead, we tailor our strategies. This adaptability is not merely about staying flexible; it’s about embedding our processes into the client's operation. For instance, when working with tech startups, we often encounter a dynamic and fast-paced environment where decisions and directions can pivot rapidly. For these clients, our project management approach focuses on agility and transparency, employing tools and methodologies that support quick changes and easy updates. Conversely, when we engage with more established companies, the emphasis might shift towards integrating with legacy systems and adhering to stricter protocols, which requires a different set of tools and a more measured pace. ![](https://cms.echobind.com/assets/1b19b593-27a9-43ad-af7e-5f51ffd95db0) ## Tools Echobind Team Members Love A critical aspect of our adaptable project management strategy is the tools we employ. At Echobind, we select tools not just based on our preference but based on what will work best for the client’s environment and project needs. Here’s a look at some of the tools our team members prefer and how they cater to different project requirements: ### Project Management Tracking: - **[Linear](https://linear.app/)**: Is the newest project management tool designed by and for software development teams. It excels in managing tasks, tracking bugs, and organizing sprints in a clean, intuitive interface that minimizes clutter and maximizes productivity. Linear makes it really easy for our internal and client teams to track milestone progress and is ideal for technical client’s that value speed in their project management processes. - **[Shortcut](https://www.shortcut.com/)**: Formerly known as Clubhouse, Shortcut is a great tool for both technical and non-technical team members. Shortcut enables teams to track progress, prioritize tasks, and comprehensive burndown reporting, making it a preferred choice for both waterfall and agile teams aiming for rapid development cycles. Shortcut is a great tool for client’s to easily review task progress, comment on features, and approve epics. - **[ClickUp](https://www.clickup.com/)**: ClickUp is versatile and can be tailored for various teams, making it suitable for both developers and creatives. It combines features of project management, document storage, and task tracking, all in one platform, making it a robust tool for managing complex projects. ### Client Communication: - **[Loom](https://www.loom.com)**: Loom has revolutionized how we communicate changes and updates to clients. By allowing us to create quick videos explaining complex issues or showcasing new features, Loom helps reduce miscommunication and speeds up approval processes. - **[Slack](https://slack.com/) or [Discord](https://discord.com/)**: Instant messaging platforms like Slack and Discord are vital in maintaining day-to-day communication. They foster a sense of community and immediacy, which is crucial for agile projects and continuous integration environments. - **[Google Hangouts](https://hangouts.google.com) or [Zoom](https://zoom.us/)**: For more formal meetings, presentations, or when dealing with clients who prefer face-to-face interaction, these tools ensure that we maintain effective communication irrespective of geographical distances. ### User Flow Mapping and UI/UX Design Approvals: - **[Whimsical](https://whimsical.com/)**: Whimsical is a versatile tool that excels in creating clear and engaging user flow diagrams, wireframes, and mind maps with a user-friendly interface that supports real-time collaboration. This tool streamlines the design approval process by facilitating easy communication and rapid feedback from all stakeholders. - **[Miro](https://miro.com/):** A visual collaboration platform that is highly beneficial for product managers involved in strategy development and project planning. It provides an expansive digital whiteboard where teams can create user flow diagrams, brainstorm ideas, map out product roadmaps, and more. Miro's strengths lie in its ability to facilitate strategic sessions and gather inputs from various stakeholders, making it an essential tool for high-level planning and decision-making in product development. - **[Figma](https://www.figma.com/):** Figma is a comprehensive design tool that enables simultaneous UI/UX design, prototyping, and feedback gathering. Its collaborative features allow team members and stakeholders to interact directly on the platform, enhancing workflow efficiency. Figma is ideal for managing design approvals due to its integrated commenting and version control capabilities. ![](https://cms.echobind.com/assets/f4f8e6e9-a12e-4a56-a04a-b47593b2eeb1) ## **Challenges and Solutions in Custom Project Management** Customizing project management to fit each client’s unique needs comes with its set of challenges. One major challenge is the alignment of tools and processes between the teams. Often, integrating new tools into an existing framework can be disruptive and may meet resistance. To counter this, Echobind focuses on extensive onboarding sessions and regular training workshops to ensure that everyone is on the same page. Another challenge is maintaining the balance between flexibility and control. While adapting to client needs, it is crucial to ensure that the project does not deviate from its core objectives and timelines. We achieve this balance by setting clear, adaptable goals and maintaining continuous communication with all stakeholders. ## **Conclusion** At Echobind, our approach to project management is not one-size-fits-all. We understand the unique challenges and needs of each project and client, adapting our tools and techniques to offer customized solutions. Our strategic thought process in organizing and tracking project progress, coupled with our flexibility in tool integration, sets us apart in delivering projects within timelines and budgets. For prospective clients looking for a digital agency that is adept at the latest project management practices and committed to customizing these practices for success, Echobind is a partner you can trust. *Claire is a project manager right here at Echobind and would love to help you hit your software goals this year. Reach out to us at [hi@echobind.com](mailto:hi@echobind.com) anytime.* --- [View on echobind.com](https://echobind.com/post/project-management-tools-and-adapting-to-client-workflows) --- # Does Your Product Need A Facelift? > Brand redesigns and a spring cleaning of your look are imperative. Let's see if it's time for you to hire a designer for an update or a full change. _By Eloisa Docton · 2024-04-26_ Is your product feeling dated? Have other competitors appeared since you launched? Your product might need a brand refresh. ## Every Brand’s Ugly Truth All brands reach a point of needing a change, sometimes it’s a simplification, an expansion, a complete rebrand, and, sometimes, a brand facelift (or refresh). There is a lot of misconception on whether or not you think you are ready for a rebrand. Rebranding takes an intense amount of research, A/B and market testing, feedback gathering, brand psychology, and the list goes on. In short, rebranding takes a large amount of effort, and budget, which is why it’s more common for a brand to undergo an update than it is to rebrand. ![](https://cms.echobind.com/assets/95c63416-33ad-40e0-9fba-f1caede96916) ## What is a Product Facelift? A brand refresh refers to the process of updating and revitalizing elements of a brand's identity, messaging, and visual aesthetics while retaining its core values and essence. It involves making strategic changes to modernize the brand and better align it with current market trends, consumer preferences, or shifts in the company's positioning. ### Here are some key components to keep in mind: ![](https://cms.echobind.com/assets/a83bb1fa-81e2-4220-8dd3-93784d034d53) ## Market Research Conducting market research and gathering customer feedback to inform the refresh process and ensure the changes resonate with the target audience. ![](https://cms.echobind.com/assets/ee02e95f-3a49-4deb-8f47-aebb2240ad06) ## Messaging A brand refresh often involves refining the brand's messaging, including its tagline, brand voice, and communication style, to ensure it resonates with target audiences and reflects the brand's current values and objectives. ![](https://cms.echobind.com/assets/99fc319d-dfe2-4911-a318-7e6a50fcbfa8) ## Brand Strategy This is when you reevaluate the brand's positioning, target audience, competitive landscape, and overall marketing strategy to identify opportunities for improvement and growth. ![](https://cms.echobind.com/assets/29627c63-416e-4014-be9e-84e013992cd4) ## Visual Identity This involves updating or modernizing the brand's logo, color palette, typography, imagery, and overall design elements to give the brand a fresh look while maintaining recognizability. ![](https://cms.echobind.com/assets/f29c4ecd-1259-4c87-a2ee-29c76257aa24) ## Transfer Findings to Product Once we have learned from our research and have created the branding refresh, we are then ready to create the new UI for your digital product. This includes background colors, buttons, fonts, graphics, and all visual elements. We also take the opportunity to polish the user’s experience to ensure that the branding, messaging and flow perform as expected. Sometimes these User Experiences require complete overhauls, but sometimes small tweaks can also provide the product with the right push to move the dial towards more positive results. This allows for small changes and bigger, more measurable impact. So, where does your brand fall in this list? We're happy to help you determine just that. *Eloisa is a senior designer here at Echobind and is ecstatic at the thought of working on some killer designs for you this year. Don't hesitate to reach out at hi@echobind.com* --- [View on echobind.com](https://echobind.com/post/does-your-product-need-a-facelift) --- # Echobind Labs Update: Let's Talk Q2 > Echobind Labs wrapped up Q1 with momentum and gears up for an even stronger Q2 with Kind Kiosk, Wayfound, ApprovalStack and SceneFlow. _By Chris Ball · 2024-04-23_ In March, I said 2024 was the [Year of Products](https://echobind.com/post/2024-the-year-of-products-at-echobind) at Echobind. We are now firmly in Q2, so let’s take a look at what we’ve been up to. ## How did our Q1 products fare? As a reminder, we announced two products for Q1. [Kind Kiosk](https://kindkiosk.com) and [Wayfound](https://wayfound.app). ### Kind Kiosk is off to a great start First, we (slightly) changed the name. “Kind Kiosk” felt better than “KindKiosk” the more we used it. We released updates to tackle some of the most requested features we heard from prospective customers. [Check out our March roundup](https://kindkiosk.com/blog/whats-new-with-kind-kiosk-march-2024). We’ve talked to numerous stand suppliers and have even started work with a local metal fabricator to create a simple, cost-effective way to mount the card reader. We’ll share more about that journey in the future. Kind Kiosk has a solid enough foundation and plenty of interest, so we’ve made it a standalone company. ### Wayfound needs more validation We’ve been using newly-created social media handles (TikTok, Instagram and X) as the primary outlet to test Wayfound. While we have generated some amount of interest via direct conversations, we need some more time to fully validate the idea with external product people. The goal of Wayfound is to provide a lightweight roadmapping solution. The software should be easy to understand and produce beautiful results with minimal effort. For client services companies, roadmaps are a constant and routine part of the job, especially for proposals. We hope we can alleviate some of the burden of creating these. As projects mature, a complex roadmapping tool that integrates into your issue tracker might be a better fit. But when you’re planning a new project where things are fluid, you need the ability to put something together quickly and iterate on it. We are learning that the space is a bit saturated and getting existing product people to try something simple is harder than it sounds. We’re doubling down on agencies as the target customer and are directly contacting folks in our network and running some small scale ads to gauge interest. --- ## Let’s talk Q2 With Wayfound requiring further validation, we spent some time thinking about our next few products. Here’s what we’ll be focusing on this quarter (alongside Kind Kiosk and Wayfound). ### Product Launch #3: ApprovalStack All client services companies work on approvals. Each of those companies have likely been in a situation where a verbal approval was given but not documented. Often, this results in some confusion or awkwardness. But it can get much worse! Clients may revisit decisions from 6 months ago and be upset about deadlines or budgets. “I never said that” can be a costly phrase. We set out to solve this problem in an elegant way. To get an approval on a major decision, all you need to do is: - Add the request to your project in ApprovalStack - Send an approval link to the designated approvers via email (optionally adding others) - The approver views the request, without needing to login. All they need to do is hit “approve” or “reject”. Rejections require a comment as to why. On top of capturing decisions and approvals, a formalized process lends weight to the decisions being made, and it can help ensure that decision makers are considering everything before the click “Approve.” Check out [ApprovalStack](https://approvalstack.com) and let us know what you think! ![](https://cms.echobind.com/assets/3d739f02-f565-4000-b3b3-ca35c28c9273) ### Product Launch #4: SceneFlow Everyone has a routine before joining a video call, starting a coding session, or setting up a livestream. You might set your video window to a certain location on the screen and add notes under it. You might set a Slack status or Do Not Disturb mode. Maybe you turn on a light and pause your music. SceneFlow aims to help you handle all of these tasks, automatically. We are working on a POC and are currently creating a landing page and branding. We are targeting a landing page launch on or before 4/29/24. Here’s a sneak peek! ![](https://cms.echobind.com/assets/79d15088-db38-4d12-9b6b-83e031011d49) ## Process Improvements We’ve started to fall into a pattern of one larger product and one smaller product per quarter. So far, it feels like a good balance, but may change as we get further along. We have been using Linear’s update feature to efficiently share multiple product updates across the team. These updates can be quite long, so it’s nice to have the shortened snapshot in our main #labs channel. ![](https://cms.echobind.com/assets/b40dbc95-c2fd-40d9-a204-69699760cc36) ![](https://cms.echobind.com/assets/35f7d53f-9caf-42aa-895a-6850909f9770) New product pitches will happen during a pitch session halfway through the quarter. Previously we were doing this during our weekly labs status meeting. We’ve also changed the format of our weekly meetings to the following: For each product: (5 min max per product) - Top Product Priority - Challenges or blockers Group Discussion (15 min max) - More on challenges and product priorities - Anyone looking for labs work to do? I’m really excited about the work our team is doing, and can’t wait to recap our Q2 progress. *Chris is Echobind’s Chief Technology Officer. Excited about Labs and want to reach out to learn more? Don’t hesitate to email us at [hi@echobind.com](mailto:hi@echobind.com) anytime.* --- [View on echobind.com](https://echobind.com/post/echobind-labs-update-how-did-q1-2024-go) --- # Why You Need A Designer For Your Project > Designers are crucial when building software and here's exactly why you need one. _By Eloisa Docton · 2024-04-18_ While design is not only about creating beautiful products, studies show that aesthetically-pleasing products evoke positive emotions, suggest higher quality, enhance user engagement, contribute to a positive brand image, and influence consumer purchasing decisions. By prioritizing aesthetics alongside functionality, companies can create products that resonate with users and drive business success. ## What does a designer do? In the digital world, there are a ton of different designers. Some are generalists, which means they can do a little bit of everything, and, some are more specialized and know their craft well. Below is a simplified explanation of the types of designers you might need for web or app development. ![](https://cms.echobind.com/assets/830a4673-0348-4c8d-8dcb-65f03c081823) - **Graphic Designers:** often work with clients or teams to understand their needs and develop creative assets like graphics, branding, and illustrations that align with brand identity and objectives. - **UX Designers:** research user needs, behaviors, and preferences to inform design decisions. They employ user testing and feedback to iterate and refine designs, aiming to optimize usability and enhance overall user satisfaction. - **UI or Visual Designers:** create visually-appealing and intuitive user interfaces. They ensure that the user is delighted through their entire experience. - **Product Designers:** work on various stages of the product lifecycle, from ideation and concept development to prototyping and production. They conduct research to understand user preferences and market trends, then use this information to design. They collaborate with cross-functional teams, including engineers, marketers, and stakeholders, to ensure that the final product aligns with business objectives and user requirements. ## 3 ways designers add value to a product Designers play a crucial role in not just creating visually-appealing products but also ensuring that users have a smooth and enjoyable experience while interacting with them. Here's a breakdown of how designers achieve this: 1. **Streamlining User Activities:** Designers analyze how users interact with a product or service and identify areas where the user experience can be simplified and streamlined. This involves removing unnecessary steps, reducing complexity, and optimizing workflows to make it easier for users to accomplish their goals efficiently. ![](https://cms.echobind.com/assets/01945bb0-6572-4022-85cc-a8cc7ba36606) 2. **Maintaining Usability and Adoption:** Designers continuously monitor the usability of the product by gathering feedback, conducting usability testing, and analyzing user behavior metrics. By staying informed about how users engage with the product, designers can identify pain points or areas of friction and make iterative improvements to enhance the overall user experience. Additionally, they ensure that new features or updates are adopted smoothly by users through intuitive design and effective communication. ![](https://cms.echobind.com/assets/d5cd3b6e-eead-4320-92bb-d83848ced7a9) 3. **Suggesting Solutions for Cost Reduction and Increased Market Share:** Designers are not only concerned with the user experience but also with the business impact of their designs. They propose solutions that not only improve usability but also contribute to lowering the cost of customer acquisition and support. By optimizing the user experience, designers can help attract and retain customers more effectively, reducing the need for costly marketing campaigns and support interventions. Moreover, by creating products that users love and find easy to use, designers can increase customer retention and loyalty, ultimately leading to increased market share for the company. ![](https://cms.echobind.com/assets/e2c80d1d-f778-41cd-9d20-2269c0ab75e5) In summary, designers play a multifaceted role in improving the user experience by streamlining activities, maintaining usability and adoption, and suggesting solutions that not only enhance the user experience but also contribute to cost reduction, increased customer retention, and market share growth. Through their expertise in design thinking and user-centered design principles, designers drive value for both users and businesses alike. *Eloisa is a senior designer here at Echobind and would love to work with you on your next big project. Reach out to us at hi@echobind.com* --- [View on echobind.com](https://echobind.com/post/why-you-need-a-designer-for-your-project) --- # Rendering Tailwind-styled PDFs from Rails View Templates using Grover > How to render Tailwind CSS-styled PDFs directly from Rails view templates using Grover. _By Deloris Thompson · 2024-04-15_ In this tutorial, we'll dive into the process of rendering Tailwind CSS-styled PDFs directly from Rails view templates using Grover. By following these steps, you'll be able to seamlessly integrate Tailwind CSS into your PDF documents while retaining the flexibility of Rails view rendering. ## **Prerequisites:** Before we begin, make sure you have the following prerequisites: - Basic knowledge of Rails - Installed Tailwind CSS and Grover gems - Access to an existing or new Rails application - Run all commands in the desired projects folder ### **Step 1: Creating the Tailwind PDF Stylesheet** To get started, let's create a new Tailwind CSS stylesheet specifically for PDFs. Open your terminal and run the following command: ```bash touch public/tailwind_pdf.css ``` ### **Step 2: Configuring Grover for Rails View Templates** Next, configure Grover in your Rails controller to render the PDF directly from a view template. Here's a sample code snippet to guide you: ```ruby # Render the view template to PDF view = ApplicationController.new.view_context html = view.render({ # Point to the template or partial you want to use partial: "", # pass any locals you need locals: {} }) # Generate the PDF using Grover pdf = Grover.new(html, # You can change the format to A3, A4, A5, Legal, Letter, Tabloid format: "A4", puppeteer: { args: ["--no-sandbox"] } ).to_pdf # Create a temporary file to store the PDF temp_pdf = Tempfile.new(["preview", ".pdf"], Dir.tmpdir) temp_pdf.write(pdf) temp_pdf.close # Log the temporary file path Rails.logger.info "PDF file created at #{temp_pdf.path}" # Return the path of the temporary file temp_pdf.path ``` ### Step 3: Adding Tailwind through a style tag We can add style tags to our PDF by passing an array of style tag options to `style_tag_options` and including our new `tailwind_pdf.css` file. Let's update Grover to use it, we can also add `printBackground` if our page contains background graphics. ```ruby # Exisiting code pdf = Grover.new(html, format: "A4", printBackground: true, style_tag_options: [{ path: "public/tailwind_pdf.css" }], puppeteer: { args: ["--no-sandbox"] }).to_pdf # Exisiting code ``` ### **Step 4: Recompiling Tailwind CSS** The `tailwindcss-rails` gem creates an input file that generates an output file containing the CSS in `app/assets/builds/tailwind.css`. Running `rails tailwindcss:build` or `rails assets:precompile` updates that output file and is the same CSS used in our application. Let’s update our Tailwind.css file by running `rails assets:precompile` and copy it to the Tailwind PDF stylesheet. Run the following command in your terminal: ```bash bundle exec rails assets:precompile # This should point to where you are compiling assets cp public/packs/tailwind.css public/tailwind_pdf.css ``` ### **Conclusion:** Following these steps, you can render Tailwind CSS-styled PDFs directly from Rails view templates using Grover. This approach allows you to maintain consistent styling across your web application and PDF documents, providing a seamless user experience. *Deloris Thompson is a senior software engineer at Echobind. Want to work with her and the entire team? Don’t hesitate to reach out at [hi@echobind.com](mailto:hi@echobind.com) anytime.* **Additional Resources:** - [Tailwind CSS Documentation](https://tailwindcss.com/docs/installation) - [Grover Documentation](https://github.com/Studiosity/grover) --- [View on echobind.com](https://echobind.com/post/rendering-tailwind-styled-pd-fs-from-rails-view-templates-using-grover) --- # Cloudflare D1 Prisma Migration Script > Cloudflare D1 and Prisma work together now, but you have to run the migrations yourself. So we wrote our own script to make the process more convenient. _By Alex Anderson · 2024-04-05_ Cloudflare D1 and Prisma [now work together](https://blog.cloudflare.com/prisma-orm-and-d1) using a new adapter! This is great if you want the convenience and developer experience that Prisma provides, while using the ultra-fast, globally-available database built into Cloudflare Workers and Pages. However, the adapter doesn't support Prisma migrations out-of-the-box. Instead, you have to generate the migrations manually using `prisma migrate diff`, and then apply them manually using `wrangler d1 migrations apply`. We wanted to simplify this process further by creating a simple migrations CLI. It automatically detects your Prisma schema and database bindings in `wrangler.toml`, but all of that is configurable. And since you're copy/pasting the file into your codebase, you can tweak it however you like. Here's how to use it: 1. Follow the instructions in the [Cloudflare blog post](https://blog.cloudflare.com/prisma-orm-and-d1) for getting Prisma and D1 set up in your project. The [gist that has the migrate script also summarizes the setup instructions](https://gist.github.com/alexanderson1993/0852a8162ebac591b62a79883a81e1a8). 2. Download the `migrate.ts` file from [the gist here](https://gist.github.com/alexanderson1993/0852a8162ebac591b62a79883a81e1a8) and put it in the `prisma/` folder in your project. 3. Add `"db:migrate":"npx tsx prisma/migrate.ts"` as a script to your `package.json` file. Now you can create and apply your migrations by running `npm run db:migrate create`. Enjoy! --- [View on echobind.com](https://echobind.com/post/cloudflare-d1-prisma-migration-script) --- # Quick Tips from a Product Designer: Brand Fonts > A senior designer's tips and tricks about using brand colors when designing for a client. _By Eloisa Docton · 2024-04-03_ Fonts play a critical role in branding, and selecting the right font is vital. Not all fonts are equal; some may require additional licensing or may not be compatible with certain technologies. ![](https://cms.echobind.com/assets/c40c6a64-1d60-4375-9eca-18c2b42e4625) ## Tip #1: Font legibility Font accessibility is paramount, with readability being a key factor. Fonts should be legible even at smaller sizes to ensure user comprehension. While there are many guidelines, there’s not a single test out there to determine if your fonts are accessible or not. Here are some of the most common things that should be ensured when it comes to fonts: - **Context of use:** Headers, body copy, data, small UI text, and CTA’s all have specific purposes, so it’s important to ensure that the fonts are tested in all scenarios to ensure the user can read your communication. - **X-height:** In general, fonts with larger x-height appear more legible, so they are often used for small captions and other small text. - **Counters and apertures:** Balanced spacing within letter curvatures and opening allow for the text to be more legible - **Stroke weight**: Not all fonts have different weights, so it’s important to find fonts that have a wide-range of weights so they can be used in the various UI elements needed in digital products. - **Color Contrast**: Text should have enough contrast to meet usability standards. There is more detailed information in [this blog post](https://echobind.com/post/quick-tips-from-a-product-designer-brand-colors) that I wrote, as well. Font accessibility is paramount, with readability being a key factor. Fonts should be legible even at smaller sizes to ensure user comprehension. ![](https://cms.echobind.com/assets/c14dced3-c970-482b-9a00-7e2a9d8065dd) ## Tip #2: Font family While it may not always be required, it is generally advisable to consider exploring readily-available fonts, such as those offered by [Google Fonts](https://fonts.google.com/). These fonts are known for their wide compatibility with digital products, making them a convenient and accessible option for various design projects. An easy way to verify what type of use a font was designed for is by paying attention to the name. Fonts with “text” in the name are typically highly-legible and designed specifically for body copy whereas fonts with "display" in the name are better for headers or copy at larger text sizes. In branding, it's typical to utilize multiple font families based on their specific use. For instance, fonts used for titles tend to be thicker and more decorative compared to those used in content. Designers often mix and match popular fonts to achieve effects. ![](https://cms.echobind.com/assets/105f5a81-8aa9-48dd-8ff0-1186180b2fce) ## Tip #3: Select a font scale with appropriate sizes for your project While we usually include approximately 8 or more text sizes for a product, generally speaking, it’s more important to understand the accessibility needs of your product to determine what will work best. However, most digital products will use something like: - Minimum size of 12pt, used for captions and some UI components that use minimal space (like system messages and captions) - Paragraph size of 16pt, used widely through the product Things like titles, subtitles, buttons, and other UI elements will have unique sizes based on the font used, the product designed, and the user needs. ![](https://cms.echobind.com/assets/774ec325-58b1-4da8-98f4-3cc7d237a753) ## Tip #4: Testing fonts While there is no one single test to ensure fonts meet usability standards, we rely on thoughtful design where we use appropriately-sized fonts for their use and screen size, and user feedback to understand the readability of the content and UI elements. Ensure font readability across different devices and environments. Consider user feedback and online resources to select the most suitable fonts for the product, even if it means introducing a new one to the brand. ![](https://cms.echobind.com/assets/2305e6e1-9cd0-4ef2-ab37-e2fa87fe8f03) ## Tip #5: Font pairing If you pay attention to the way this very blog post is laid out, and how the fonts are used, you can see a great example of font pairing. We deep dive into hierarchy [in this post](https://echobind.com/post/5-minute-redesigns-improving-hierarchy-in-2-steps) from last month. Things to take into consideration: - Titles are typically large-scale and heavier in weight. They serve as prominent identifiers for sections or topics. Sans fonts are commonly used for titles, while Sans Serif fonts in heavier weights, all caps, or with other stylistic variations are also popular choices to make them stand out. - Paragraph fonts are scaled to fit the reader's needs, typically ranging from 14px to 20px. They are the main body of text and should be chosen for readability and legibility. - Subtitles are scaled larger than paragraph text but smaller than titles. They provide additional context or information beneath the main title. Like titles, they may also be heavier in weight for emphasis. - Table Fonts are used in tables and should prioritize readability, especially considering the smaller scale of information within cells. Sans fonts are often avoided in favor of fonts with better legibility at small sizes. - CTA Fonts (call to actions, like buttons and links) will often be a stylized version of the paragraph font, heavier weights, captions, spacing are often variations. - UI Fonts are used to help the user through their journey, such as alerts, error messages, and descriptions, etc. They're usually scaled in the smallest readable text, around 12px. ![](https://cms.echobind.com/assets/b60536d3-65d4-4aa7-8e65-74543b2589c9) ## In Short Every single one of these tips is a simplified recommendation, and they actually work! Some key takeaways: - Fonts have a very important role in digital products; they help the user understand the screens and what is being asked of them. - It takes the user a shorter amount of time to complete tasks as the eyes will signal the brain as it matches the way the font looks to the context. - The use of certain fonts need to be excluded from digital products because they don’t play nice with technology. - As always, accessibility should always triumph over the use of branding fonts. *Eloisa is a senior designer here at Echobind. If you're interested in working with us, don't hesitate to reach out at hi@echobind.com* --- [View on echobind.com](https://echobind.com/post/quick-tips-from-a-product-designer-brand-fonts) --- # Client Satisfaction & Feedback: A Guide for Account Managers > Experienced account managers know how to obtain client feedback effectively and use it in best ways possible for their agency. _By Lex Meola · 2024-04-01_ Welcome to the high-stakes world of account management, where the satisfaction of your clients reigns supreme. As an account manager, your ability to navigate the delicate balance of client satisfaction and feedback directly correlates to amount of success you’ll have. Let's dive into the crucial aspect of your role with a blend of professionalism and a sprinkling of humor. 1. **The Power of Satisfaction**: Client satisfaction isn't just a metric; it's the lifeblood of your business. Satisfied clients are not only more likely to remain loyal but also to become ambassadors for your brand. It's like having a loyal fanbase who can't stop singing your praises – and who doesn't want that? No one. The answer is no one. Everyone wants it. 2. **Embracing Feedback**: Feedback, while sometimes daunting, is an invaluable tool for growth. It's the compass that guides you towards improvement and innovation. Think of it as the GPS for your journey towards client satisfaction – occasionally recalculating, but always leading you in the right direction. 3. **The Art of Active Listening**: As an account manager, your ability to actively listen is your secret weapon. It's not just about hearing what your clients say; it's about understanding their needs, concerns, and aspirations. Think of it as tuning in to your favorite podcast – except instead of improving your health and wellness or nerding out on true crime, you're dialing in to the voice of your client to improve their experience. 4. **Finding the Balance**: Balancing the demands of multiple clients can sometimes feel like juggling chainsaws while riding a unicycle – challenging, but not impossible. The key is prioritization and effective time management. Know when to say yes, when to say no, and when to ask for help. After all, even the most skilled jugglers need a safety net. I was at a show once where they didn’t put one up. It was…devastating to say the least. The bowling pins were fine though. 5. **Turning Challenges into Opportunities**: Every challenge is an opportunity in disguise. Whether it's a client complaint or a missed deadline, approach it with a solutions-oriented mindset. Think of it as turning lemons into lemonade – except instead of a refreshing beverage, you're serving up stellar client service. 6. **Continuous Improvement**: The pursuit of client satisfaction is a journey, not a destination. It's a marathon, not a sprint. Embrace a culture of continuous improvement, always striving to raise the bar higher. Think of it as climbing a never-ending staircase – challenging, but with breathtaking views at every step. In conclusion, navigating client satisfaction and feedback is both an art and a science. It requires a delicate balance of professionalism, empathy, and a willingness to embrace feedback with open arms. So, my fellow account managers, let's approach this challenge with confidence, determination, and just a hint of humor – because after all, laughter is the best medicine for even the most daunting tasks. *Lex Meola is Echobind’s account manager; feel free to [schedule a 15-minute call with her](https://savvycal.com/lexmeola/dcf1e6ba?sid=56a72085-047c-424e-b277-a17fb391a84a&from=2024-03-31) during the week via her SavvyCal. Whether you [want to hire Echobind](https://www.echobind.com/contact) or just need someone to chat account management with, she’s a great resource for it all.* --- [View on echobind.com](https://echobind.com/post/navigating-client-satisfaction-and-feedback-a-professional-guide-for-account-managers) --- # A Developer’s First Two Weeks with an Apple Vision Pro > A software developer spent two weeks with the Apple Vision Pro and gives his hot takes. _By Brandon Richey · 2024-03-29_ Last month, Apple finally shipped the long-rumored (and possibly one of their worst-kept secrets) Apple Vision Pro. I'll admit that my initial reactions were a healthy mix of curiosity and skepticism. As a developer, I'm constantly seeking tools that not only enhance our productivity (and get me closer to my childhood vision of a totally VR computing space, thank you Hackers "flying through the computer" montages). Enter the Vision Pro, a device that has been making waves for its innovative approach to spatial computing and app integration. After spending one month with the Vision Pro, here's a deep dive into my experience and the valuable lessons learned. ## Is the Simulator Enough? Short version: no. Apple thankfully provides some really great device simulators to help you build products with XCode, and their Vision OS device simulators are thankfully continuing this tradition. However, throughout my time with the device, it became clear that simulators pale in comparison when building complex applications, especially if those applications rely on either spatial functionality or AR functionality. Using it with the actual Vision Pro offers a unique, immersive testing environment that simulators simply cannot replicate, providing invaluable insights into the user experience of spatial and augmented reality apps. If you're building Apple Vision Pro apps, then yes, you're definitely going to need one at some point.

## The Ultimate Productivity Companion The Vision Pro proved to be the perfect sidekick for couch-based productivity, and I say this as someone with a REALLY big monitor for development. Pairing it with my Macbook Pro opened up a new dimension (see what I did there?) of comfort and efficiency, making it an indispensable tool for developers who cherish the flexibility of working from any corner of their home, without losing any of the critical screen real-estate I've come to expect from my stationary work desk. Even after a month of using it, doing anything on the couch (or frankly anywhere away from my main development rig) usually means picking up the Vision Pro first and taking it and my laptop with me. ## A Developer's Dream: Tools and SwiftUI Developing for the Vision Pro is not just feasible; it's a pleasure. The plethora of tools available, coupled with the simplicity and power of SwiftUI, makes developing engaging apps a breeze. Whether it's designing intuitive UIs or integrating complex functionalities, the Vision Pro environment is surprisingly developer-friendly, making it an exciting platform for innovation. SwiftUI is a great framework to build apps in and it's great that they put it front and center when building new Apple VisionOS apps. ## BYOGDS (Bring your own game development skills) For those versed in game development, transitioning to creating spatial apps for the Vision Pro feels like a natural progression. The skills acquired in game development, such as spatial reasoning and 3D modeling, are directly applicable and highly beneficial. This cross-compatibility not only eases the learning curve but also enhances the quality and depth of the apps developed. Hope you know your quaternions! I haven't messed around with Unity's tooling yet, but that's on my list. ## Comfort: A Work in Progress While the Vision Pro excels in many areas, comfort is one aspect that needs attention. The single nylon strap falls short in providing the necessary support for extended use. Investing in a dual strap for the top and back of the head is essential for achieving the comfort level required for long development sessions or simply enjoying the device for leisure. I've seen all kinds of homebrew bands and things that hope to fix or at least alleviate some of this, but VR raccoon-face remains an issue even after years of VR headsets. ## The Uncanny Valley of Personas ![](https://cms.echobind.com/assets/5b7eed90-bc29-45a9-b7bf-3016ecdccb19) *Hands also suffer from the uncanny valley: it looks very virtual and there’s no “hand scanning” functionality, so it’s just enough to be a little off-putting* The attempt at creating lifelike avatars or "Personas" within the Vision Pro ecosystem is ambitious. However, it currently resides in the uncanny valley, where the avatars are realistic enough to be unsettling yet not lifelike enough to feel genuine. The few meetings I've tried so far with Zoom and their persona avatar concept were off-putting and generally just creepy. That is, at least when the meetings worked... ## And the Sheer Cliff of Audio Troubles with Zoom ![](https://cms.echobind.com/assets/63e4bb59-d5af-4740-bc5b-1b646a88837c) *Frowning also doesn’t work.* ...because in an era where remote work and virtual meetings are the norm, the Vision Pro's integration with popular apps like Zoom is crucial, and the app just does not function well. This is a significant drawback for developers and professionals who rely on seamless communication for collaboration. As soon as those issues are fixed, this app will be more usable. Until then, I wouldn't hope for this to be your day-to-day meeting and work device. Unless you want to be *that* person. Persona. Whatever you want to call it. ## Ergonomics and Battery Life: A Balancing Act The Vision Pro strikes a good balance in terms of ergonomics and battery life. While it's slightly on the heavier side, it's not a deal-breaker. The device manages to maintain a level of comfort that supports prolonged usage, and the battery life has proven to be satisfactory for day-to-day tasks and development work. Overall, I give it a B+. ## Not Just Another Gaming Device Apple's decision to orient the Vision Pro towards apps, rather than gaming, is a strategic move that aligns well with its ecosystem. The "hand tracking by default" decision significantly reduces friction, making the device more accessible and intuitive for a wide range of applications. This focus has the potential to redefine how we interact with digital content and develop applications. It also really only takes that 30-minute demo in the Apple Store to "get it"; after that pinching, pulling, zooming, and moving feels pretty intuitive, and holding the dial to recenter/regroup windows feels quick and simple. ## The Price Point Dilemma The Vision Pro's capabilities are undeniably impressive, but its adoption is ultimately tied to its price point. At its current pricing, it remains a premium offering that may not be accessible to all Apple enthusiasts. However, I'm convinced that if the device were priced at $1,500 or less, it would become a staple in every Apple home, given its potential to revolutionize productivity and entertainment. Watching Mad Max: Fury Road on this thing, in full 3D, in my own personal theater, in super high resolution was an absolute dream and in my mind is *THE* way to watch that movie. I have yet to try anything else on it, though I'm excited to watch a lot more! ## Conclusion If the Vision Pro is indicative of where the AR/VR/XR/Spatial ecosystem is going, then I'm really excited to see how things shape out. There are some hurdles, to be sure, and that price point is...frankly not going to win anyone over. But if you look at the Vision Pro as more of a preview of coming attractions, it's hard to not get excited about what that really could mean for developers, for consumers, and for everyone in-between. It's the perfect merger of entertainment, productivity, and technology, and as the price goes down, the functionality gets more refined, and more developers start to envision what we can really do with technology like this, I can't help but be excited about what a broader consumer audience for this could bring. ![](https://cms.echobind.com/assets/1c4a8710-c150-4477-853e-f41b111acfa3) I still don't have my "flying through my file system in 3D" functionality yet, though. **Brandon is a senior software engineer here at Echobind. [Reach out to him on X](https://twitter.com/diamondgfx) anytime or email us at hi@echobind.com to build software for you within the spatial ecosystem Brandon wrote about above.** --- [View on echobind.com](https://echobind.com/post/a-developer-s-first-two-weeks-with-an-apple-vision-pro) --- # Quick Tips from a Product Designer: Brand Colors > Read these quick-hitter tips and tricks all about brand colors and how to properly use and understand them in your design work. _By Eloisa Docton · 2024-03-25_ We often work with clients who have branding guidelines that they acquired from agencies or in-house teams. These guidelines usually focus more on marketing aspects like merchandise, online presence, and social media while overlooking user experience (UX). ![](https://cms.echobind.com/assets/cce38ff3-09fe-45c8-849e-fc44b5c9473a) When creating digital products, usability and accessibility are prioritized. Sometimes, branding guidelines don’t quite meet usability standards, requiring extra work to prioritize usability over branding. This could be avoided by considering usability needs early in the branding process. While creating a user-friendly product requires research, knowledge, tools, and testing, there are simple tips to start the branding and UX journey effectively. ## Colors ![](https://cms.echobind.com/assets/27ebf829-1b41-48b8-8b51-d3ae2339f862) There's a vast array of colors available for digital use. It's essential to consider color combinations and their intended purposes. Users associate certain colors and symbols with specific actions or feelings, based on color psychology. For instance, red signifies passion and alertness, making it suitable for highlighting important information like errors. ![](https://cms.echobind.com/assets/ac8e5a62-3801-4127-83c7-036f4f9667e4) When creating palettes, there is also a need for multiple neutral shades. We use those for backgrounds and UI elements. Having multiple colors to choose from ensures tighter brand control and harmony across products. ![](https://cms.echobind.com/assets/e8bae6f8-9984-4b37-9c89-a988ba9c6e66) When designing branding, it's important to avoid overwhelming users with excessive use of colors. Colors like red, green, and yellow also convey messages and should be used judiciously, as these colors are also used for common UI for semantics. Color combinations should prioritize contrast for usability. Testing color usability against background and text is crucial, with resources like the [WebAIM website](https://webaim.org/articles/contrast/) offering valuable insights. ![](https://cms.echobind.com/assets/1c2ea604-f5fb-4bd3-80dc-fffe644e2e38) ## Example Let's examine a sample brand kit comprised of a logo, primary colors, and designated fonts. ![](https://cms.echobind.com/assets/8b1bc441-9715-4d98-80d3-f72ae5f8091f) We begin by evaluating the colors designated for links and buttons, which are common UI elements in most products. For this example, we’ll use magenta: ![](https://cms.echobind.com/assets/e011120d-3dae-419b-bd88-00d95c9cc704) In our tests, it's evident that magenta does not contrast effectively with white but performs better with black. However, considering the accessibility rating, it appears that small fonts may only achieve partial accessibility. In this case, we may need to explore tints of the color, which derive from the main color. They are lighter and darker shades of the same color. We’ll lean on lighter for this exercise, as the background is dark; however, if you have a light color background, it will be best to lean towards darker tones. ![](https://cms.echobind.com/assets/af105c99-2517-40e2-a5c4-19aefef6def6) The additional colors are derived from the original magenta. Now, we can test those against black or white to see if we can get to a better usability rating: ![](https://cms.echobind.com/assets/a0218c0c-0dfe-4c97-97d5-d0505f7f1c26) Now that we’ve updated the magenta to a lighter tint, the accessibility rating on the black text increases, so this is what we would use. In a real-life scenario, we may repeat this exercise with each color if we find it necessary, use sophisticated tools, and even test with users to land on the best option for your product. That’s it! In summary, integrating minor UX considerations at the outset enhances the design process and minimizes the need for extensive brand adjustments and unnecessary marketing revisions. *Eloisa is a senior designer here at Echobind. If you're interested in working with us, don't hesitate to reach out at hi@echobind.com* --- [View on echobind.com](https://echobind.com/post/quick-tips-from-a-product-designer-brand-colors) --- # Demystifying ESLint Configurations: A Helpful ESLint CLI Command > Frustrated by conflicting ESLint configs? This microblog entry reveals a magical CLI command to debug hierarchies and untangle settings. _By Mike Cavaliere · 2024-03-18_ Hey there, fellow developers! I want to drop a quick but incredibly handy tip that's been a lifesaver for me, especially when wrestling with those pesky linter configs and VSCode auto-formatting quirks. Here it is: **`eslint --print-config `**. Yep, that's the command you need to keep in your toolkit (docs [here](https://eslint.org/docs/latest/use/command-line-interface#--print-config)). Why is it so useful, you ask? Well, this nifty little command does something super valuable – it reveals the actual ESLint configuration applied to your JavaScript file. We've all been there, setting up ESLint, tweaking rules, only to find our VSCode doing its own thing when auto-formatting. It's like that one puzzle piece that just won’t fit, no matter how hard you try. But, fear not! This command is like the magnifying glass that brings into focus what's really going on with your configurations. So next time you find yourself in a tangle with ESLint rules, or VSCode seems to have a mind of its own, remember this command. It’s a straightforward, no-nonsense way to get a clear view of the ESLint configurations at play. Debugging and configuring become much more manageable, saving you time and, let's be honest, a bit of sanity. Keep coding and stay efficient! *Mike is a senior software engineer at Echobind; [give him a follow on X](https://twitter.com/mcavaliere).* --- [View on echobind.com](https://echobind.com/post/demystifying-es-lint-configurations-a-helpful-es-lint-cli-command) --- # 10 Terms All Account Managers Need To Know When Working With Software Engineers > Here are 10 terms all account managers need to know when working with software engineers. _By Lex Meola · 2024-03-13_ I remember it like it was yesterday. I was sitting in a conference room with four senior engineers. One of whom everyone at the company referred to as “Zucky,” akin to Mark Zuckerberg. The imposter syndrome wasn’t just in the room with me, it was breathing down my neck, constantly reminding me that I didn’t understand the context of the meeting and therefore, didn’t deserve to be there. For those of us who are more business-focused, participating in a discussion that is overtly technical can feel painful. As the imposter syndrome creeps in, we feel uncomfortable and quite frankly, lost. I’d love to go over 10 terms I got a hold of that have helped drive technical conversations and reduce the discomfort I experience when I lose the plot of a discussion. ## **Let’s break it down** Account managers serve as a crucial bridge between clients and internal teams, ensuring smooth communication and successful project outcomes. When collaborating with software engineers, having a solid understanding of technical terminology is essential for effective coordination and relationship-building. In this blog post, we'll explore the top 10 terms every account manager should know when working with software engineers: 1. **Agile Methodology:** Agile is a project management framework characterized by iterative development, where requirements and solutions evolve through collaboration between self-organizing cross-functional teams. Understanding Agile principles helps account managers grasp project timelines, priorities, and the iterative nature of software development. 2. **User Story:** A user story is a concise description of a software feature from an end-user perspective, typically written in non-technical language. Account managers should understand user stories to effectively communicate client needs and priorities to the engineering team. 3. **Backlog:** The backlog is a prioritized list of features, enhancements, and fixes that need to be addressed in a software project. Account managers should be familiar with backlog management tools like [Jira](https://www.atlassian.com/software/jira) or [Trello](https://trello.com/) to track project progress and prioritize tasks effectively. 4. **API (Application Programming Interface):** An API is a set of rules and protocols that allows different software applications to communicate with each other. Account managers should understand APIs to facilitate discussions around integrations, data exchange, and system interoperability. 5. **MVP (Minimum Viable Product):** The MVP is the simplest version of a product that satisfies customer needs and allows for iterative development based on user feedback. Account managers should understand the concept of MVP to manage client expectations and prioritize features for early release. 6. **Deployment:** Deployment refers to the process of releasing a software application or feature into a production environment for end-users to access. Account managers should understand deployment pipelines, version control systems, and release management practices to coordinate product releases effectively. 7. **Testing (Unit, Integration, Regression):** Testing is a crucial aspect of software development to ensure product quality and reliability. Account managers should understand different types of testing, including unit testing (testing individual components), integration testing (testing interactions between components), and regression testing (ensuring new changes don't break existing functionality). 8. **Scalability:** Scalability refers to a system's ability to handle increasing workload or user demand without sacrificing performance. Account managers should understand scalability considerations to anticipate future growth and ensure the software can accommodate evolving needs. 9. **DevOps:** DevOps is a cultural and technical movement that emphasizes collaboration and automation between software development and IT operations teams. Account managers should understand DevOps principles to facilitate communication and collaboration between development and operations teams. 10. **SLA (Service Level Agreement):** An SLA is a contractual agreement between a service provider and a client that defines the level of service expected, including metrics such as uptime, response time, and resolution time. Account managers should understand SLAs to manage client expectations and ensure service delivery meets agreed-upon standards. I can’t stress it enough: Effective communication between account managers and software engineers is essential for successful project delivery. By familiarizing themselves with these top 10 terms, account managers can enhance collaboration, streamline project management, and ultimately deliver better outcomes for clients and stakeholders. *Lex Meola is [Echobind’s trusty account manager](https://echobind.com/post/lex-is-ready-to-talk-to-you). Reach out to her at any time (lex@echobind.com) if you’re interested in working with us, or even [grab 15 minutes on her SavvyCal](https://savvycal.com/lexmeola/dcf1e6ba?sid=b20642b3-ce77-4271-bbbd-d6db4301d5b3).* --- [View on echobind.com](https://echobind.com/post/10-terms-all-account-managers-need-to-know-when-working-with-software-engineers) --- # A Simple Start to Caching > Speed up apps by caching work in the right place. _By Alex Anderson · 2024-03-11_ Apps do work; work takes time. Spend too much time doing work, and users get frustrated. Naturally, you can try to speed up the work somehow - make your code do less work, optimize slow code paths, put your servers closer to databases. But alas, for some situations even the fastest programs are still too slow. Fortunately, much of the work to do is work that’s already been done before. Can’t we just… reuse that work? ![](https://cms.echobind.com/assets/1ffc9976-4432-4890-865a-8f5aafd6c09d) *It’s not cheating, it’s just looking at work that’s already been done.* In the context of web apps, that work could be CPU-bound, like performing a costly calculation, or IO-bound, like downloading files from the server or fetching data from a database By storing the result of that work, you can skip the work the next time you need it. The basic principle is simple: Every possible bit of data is identified with some key, like `user-1` or `/favicon.ico` or `fibonacci-25`. Before getting the data from its source, we check the cache to see if it’s already there. If it is, great! We return the data. If not, we do the expensive operation, put the result in the cache, and return that to the user. Sounds simple, right? What could possibly go wrong? ### Cache Keys For starters, you’ve got to be careful about making sure you put enough information into your keys. Take this filter menu for example. ![](https://cms.echobind.com/assets/18106c9c-48db-4cfb-b397-fd1a40c1e9bf) Suppose we wanted to cache the results for a user. Here, we’ve got five categories of things. Type and Color can be multiple values at the same time, while Price can be a granular scale of values. How do you come up with a cache key for this data? It’s important to remember that cache keys need to exactly represent the data they store. Suppose we forget to include the `color` input in our cache key. Maybe the cache key is something like `${sort}-${inStock}-${priceLow}-${priceHigh}-${types.join(',')}`. Our first user comes, filters by red products, and the results get put in the cache with the key `selling-false-0-35-`, which is the default key. Then the next user comes along and filters by yellow. The cache key is `selling-false-0-35-`. See the problem? Because we’re missing `color` in our cache key, user 2 will get the same results as user 1, even though they requested a totally different set of colors. This also illustrates how as your cached function has more inputs, the cache key becomes more complicated. Eventually your cache key becomes so granular that it might not even be worth it to cache the data at all. It wouldn’t be a problem if we had unlimited storage, but alas - you will eventually run out of space and something will have to be removed from the cache. But there’s another, likely more important, reason for removing things from the cache - eventually the underlying data will change. Unless the cache is updated with it, now your cache is returning outdated data. ### Cache Invalidation & Eviction > There are two hard things in computer science: cache invalidation, naming things, and off-by-one errors. > In an ideal world, any time the underlying data changes, you could determine exactly what cache keys use that data and just delete those items. This provides the most granular, exact caching that is always up-to-date, but it can take effort to do well. Alternatively, the cache itself can make sure the data is fresh. Perhaps it includes a `modified` date that it can use to quickly query the source database to know if the data has changed. A safer fallback - one that risks returning outdated data, but can still work most of the time - is to set some kind of cache expiration or eviction policy. For example: - items only remain in the cache for n seconds - items are removed from the cache at a specific time - the cache only contains n number of items, removing the oldest item (LRU, or least-recently used), infrequently accessed items (LFU, or least-frequently used), or a random item when a new item is added - treating items as stale after a certain time and returning the old cached entry while new data is fetched - also knows as “stale while revalidate” Which combination of strategies you use depends on what the data is and where it is cached. For example, a static website might use the `Cache-Control` header to have the browser cache the site for a certain amount of time, while a dynamic filter like we saw above might only keep a certain number of cached filters using a server-side LRU cache. ### Cache Storage For web apps, there’s a whole spectrum of places where you might want to cache: - Server Cache: server-side, sometimes private, cached data that persists between requests. Might be shared between servers using an external data store like Redis. - Request Cache: cached data that persists for the duration of a request to avoid multiple fetches to the same resource and solving the n+1 problem in GraphQL servers. [Dataloader](https://github.com/graphql/dataloader) and [React.cache](https://react.dev/reference/react/cache) are both examples of request caches. - CDN Cache, or Shared Cache: cached data that is replicated in datacenters across the world. The data might come from multiple servers and be consumed by multiple clients. Shared caches can be instructed to cache HTTP responses using the `Cache-Control: s-maxage` directive. - HTTP Cache: the browser version of Shared Caches. Also controlled using the `Cache-Control` header. There’s no real way to invalidate the HTTP Cache without the user specifically clearing it, so it’s good to be cautious with long expires times. It works especially well for static assets that don’t change frequently, like images, CSS files, or JavaScript. Often these assets have content hashes in their filenames, which treats them as new cache entries when the filename changes. - Browser Cache: persistent storage within browsers that can be accessed through JavaScript and Service Workers. This includes the [Caches API](https://developer.mozilla.org/en-US/docs/Web/API/Cache), which service workers can use to replace responses to requests to support offline access, [IndexedDB](https://web.dev/articles/indexeddb) for persistent storage, or in-memory caches like [TanStack Query](https://tanstack.com/query/latest) that are cleared when the page is closed or refreshed. Each of these has different uses, capabilities and limitations. They can be used individually, or together within the same app to speed it up. Naturally you don’t have to use all of them at the same time. As a rule of thumb, start with HTTP caching followed by Server Caching. Those two will provide the biggest speed improvements with relatively low effort. --- Hopefully this helps clear up some of the basics of caching. In the future we hope to have a whole series of blog posts going into the intricacies of each of these caching strategies. Until then, may your data be fresh and your caches invalidate when you want them to. *Alex is a senior software engineer at Echobind. [Follow him on X](https://twitter.com/ralex1993).* --- [View on echobind.com](https://echobind.com/post/a-simple-start-to-caching) --- # 2024: The Year of Products at Echobind > Echobind is building new products in 2024 thanks to our new "labs" initiative. Introducing KindKiosk and Wayfound. _By Chris Ball · 2024-03-06_ I’m excited to officially announce the start of something new for Echobind. We’re building and launching products! ## A long time coming At some point, every agency (and for that matter every developer, designer, and product person) considers building their own products. Most of us have a side project graveyard full of half-baked ideas as proof. I’ve talked about products at Echobind for *years*, and in 2024 we’re committing to it. ## Why? Many reasons. As Echobind the company, we gain: - new sources for potential leads - additional credibility by showing what we’re capable of - opportunity to diversify revenue - a chance to own and continuously iterate on something As Echobind the team, we: - learn through building. We’ve built and shipped many products on behalf of our clients. Labs—our official entity name for working on these products—will be our space to continuously build, explore, and refine our own ideas. - get an opportunity to wear different hats. Our engineers can work on product. Our designers can work on marketing. Our strategy team can do sales. It’s a great way to expand our individual skill sets. As a bonus, we gain a better understanding for what our coworkers go through on client projects! - have a place to experiment with cutting-edge technology, like the **[Vision Pro](https://www.apple.com/apple-vision-pro/)** (more on that soon!) Because the consulting side of Echobind covers salaries and expenses, Labs can focus entirely on product validation, execution, and generating revenue. That significantly reduces our risk, and provides an easier path to profitability. Speaking of profitability, we are treating Labs as a separate entity from Echobind. It’s all the same crew, but different revenue and expenses. ![](https://cms.echobind.com/assets/32654cb8-91eb-411c-bdb9-188524e10a96) ## 2024 Roadmap - **Get Organized.** In February, we established the **PROduct Validation and Evaluation (PROVE)** team. PROVE exists to ensure all of our ideas keep moving, and is a cross-discipline group of product managers, designers, and developers. Anyone on the team can join the Friday PROVE huddle. During the huddle, we give overall Labs updates and product pitches. We talk about our approach and framework. It’s off to a great start. - **Set Goals.** We will launch two products per quarter. That’s aggressive, but it’s also attainable. There’s no time to waste, which forces us to focus. There’s only one caveat: if any product gains significant traction, we may pause launches temporarily to direct energy appropriately. That would be a good problem to have. - **Market, Analyze, and Grow.** Product marketing is an area we don’t often get to focus on at Echobind. We will now. All products and marketing sites will leverage [Posthog](https://posthog.com) at launch, and we’ll use that data to drive product decisions. - **Review and Decide.** Each quarter we will review the performance of all products and decide on next steps. We will look for strong indicators of interest (or lack thereof), evaluate the effort we’ve put in, and read conversations with potential customers to understand how products are being used. So with that, it’s time to unveil what we’re working on in Q1! ## Product Launch #1: Kind Kiosk [Kind Kiosk](http://www.kindkiosk.com) is a standalone, customizable kiosk used to increase donations. With just a few taps, donors can give via card, Apple Pay, or Google Pay. In an age of QR codes and countless online donation pages, we’re focused on the in-person experience. Those magical moments at an event or physical space where donors are most likely to give. ![](https://cms.echobind.com/assets/56cdae8a-d35a-43bd-b641-0795830ba429) *The donor experience can be customized to match your brand. The designation for donations, copy, and “quick donate” buttons are all configurable.* We’ve been working on Kind Kiosk with limited bandwidth for a while now. With the formation of Echobind Labs, we’re excited to say it will officially launch at the [Nonprofit Technology Conference](https://www.nten.org/gather/ntc) in Portland, OR on March 13th. ![](https://cms.echobind.com/assets/901aa4f0-bc4c-4b50-be59-c5dcb95f003d) *The Nonprofit Technology Conference is the official launch for Kind Kiosk.* Imagine a fundraising dinner, basketball game, museum, zoo, or animal shelter. How about a kiosk in the office that allows employees to give to a different charity each month? There are so many exciting possibilities. We can’t wait to see how it’s used. ![](https://cms.echobind.com/assets/2d419dc5-f098-4000-ade8-deee22d4c23d) *The admin dashboard. Understand how each kiosk performs, and get a snapshot of your donations.* Kind Kiosk has been a really fun product to build. It’s not a typical SaaS. It uses hardware. We’re taking a fully-managed “white glove” approach. Though the overall process feels familiar, there’s quite a bit that feels new. On a personal note, I’m really proud of this one. If you’re interested in learning more, visit [kindkiosk.com](http://kindkiosk.com?utm_source=ebblog&utm_medium=web) or [book an intro call](https://savvycal.com/kindkiosk/intro) with us! ![](https://cms.echobind.com/assets/fee11815-b328-46d5-8150-db00c4a5ea92) *Our launch stickers and a test run of branded packing tape. Packing up and shipping the kiosk hardware should be an interesting endeavor!* --- ## Product Launch #2: Wayfound Our second product for Q1 is [Wayfound](https://wayfound.app/). We’ve used every roadmapping software available, but we always find ourselves defaulting to using Google Sheets. It’s easy. The problem with Sheets is that it’s not a great deliverable. We want to wow our clients at every step. So we set out to create something that’s almost as easy to use as Sheets, and looks beautiful in a proposal or as a final deliverable. ![](https://cms.echobind.com/assets/9c1ad41a-98c9-4d6b-9cfd-bf791ef52a1e) *The Wayfound waitlist page. Yes the logo looks a little bit like React. We don’t care at this stage.* We’ve also tried using [Shortcut](https://www.shortcut.com) and [Linear](https://linear.app) to generate a roadmap. As great as those tools are, they don’t work well for us in proposals or early deliverables, especially when on-the-fly edits are needed. Once a project is in the design or build phases and has tickets to work against, they work much better. Check out Wayfound ([https://wayfound.app](https://wayfound.app?utm_source=blog)) to learn more and join the waitlist. If your team might use a product like this, we’d love to [hear from you](mailto:hi@echobind.com). ## Closing out Q1 We have four weeks to finish the MVP for Wayfound. Q2 is just around the corner. The PROVE team has heard multiple product pitches, and we have more on the way each week. Look for a Q1 Labs recap post at the end of the month and more about our plans for Q2. We’re excited to keep shipping! *Chris Ball is Echobind’s CTO and CoFounder; [connect with him on LinkedIn](https://www.linkedin.com/in/chris-r-ball/) any time.* --- [View on echobind.com](https://echobind.com/post/2024-the-year-of-products-at-echobind) --- # Noise Reduction in Core Concept Conversations > A breakdown by product manager Zack Marty about the types of "noise" that exist in his agency work, and how to approach it when it arises. _By Zack Marty · 2024-03-05_ Noise is one of the many reasons that most office workers don’t have their offices located adjacent to a concert venue. Noise makes it hard to concentrate, it doesn’t add anything beneficial to our work, and it makes me wonder why anyone thought that the open-concept office space would be of any benefit to people who need to do any kind of deep work. Noise, especially the kind that we can’t predict, has a way of making everything else more difficult. When we do tasks that require a bit of thinking, we often find ways to reduce the noise around us to aid in concentration. So when I talk about noise in reference to product management and software development, I am referring to all of the added stuff that doesn’t benefit the core conversation being had. Noise reduction applies to many industries, but today we will focus on product management and software development. ### The added stuff When we have conversations about how features will work, we typically use hypothetical scenarios to explore a given feature. This allows us to connect with the feature and talk it through in a way that gets us further away from the theoretical implementation and a little closer to how it will actually work. In the below example, one of our clients wanted to build a semi-social mobile application and had a specific way that they wanted the sorting algorithm to work on a user’s feed. The goal was to ensure that people received attention on their social posts rather than the way many apps currently work where the goal is to get social posts to snowball. Let’s take a look below at how their proposed solution would work. ![](https://cms.echobind.com/assets/60d8a236-b842-44ba-b8d0-eed54a483ba7) ### What makes this noisy? This example is relatively simple since their proposed version of the sorting algorithm doesn’t take any sort of time decay into account. If a user’s social post receives ‘likes’ of some sort, then it gets thrown to the bottom of the pile. They are essentially punishing users for receiving attention on their social posts. It was not hard to convince our client that this would not be a good way to implement the sorting algorithm. So, where is the noise? The noise comes from the fact that the social posts themselves have absolutely nothing to do with the sorting algorithm. We don’t need to see that the first social post involved going to the mall or that the third social post shows the user doing a cartwheel. So how do we remove some of the noise? ![](https://cms.echobind.com/assets/7f988c9b-b4f6-4417-b6b5-43261c98c459) In this version, we can easily show off the sorting algorithm without needing to show the contents of the social posts themselves. This is a great way to reduce noise. Rather than showing actual posts, we just have placeholders. Is there a way that we can reduce noise even further? Yes. In the above version, we create noise by using almost identical text. It takes a moment or two in order to identify and indicate to what you are referring. In more complex scenarios, talking about ‘social post 1’ vs ‘social post 2’ is surprisingly laborious and can confuse people when there are no major demarcations between the posts. While we reduced noise in one way, we also made more noise since it is easier to identify skirts and cartwheels than social post 1 and social post 2. ### Why not just use numbers? The next question that should arise is, “Why not just use numbers? One and two are distinct values.” The issue we create here is that there is a value and sequence cognitive load that accompanies numbers due to our long relationship with numbers as people. Again, it simultaneously reduces and increases noise. My suggestion is below: ![](https://cms.echobind.com/assets/a1437b31-3776-4237-89c7-8e17bda2163a) In the above example, each of the placeholders for social posts are conceptually and visually distinct. When I first made this visualization, I used a panda where the crab is, but I replaced it with a crab so that it would be more conceptually distinct from the bear despite them both being visually distinct. There are plenty of other visuals that you can use to create distinct journeys for your audience to follow, but I like the animal images since they are typically known by the audience rather than something I might need to explain to someone. However, despite all of our concentration on visuals, the animals / social posts are not as important as the core concept to discuss, which is the sorting algorithm. The animal pictures just allow us to think about placeholders less than we think about our core concept. ### Do you have other examples? Of course, I would not have made the above title if I didn’t. In a discussion with a client, we need our client to help us understand the relationship between a few different people who would be using or connected to their software. For their crowdfunding platform, we needed to better understand the relationships between the different parties. Let’s start by dumping the explanation into a paragraph. A campaign organizer can create a campaign for themselves or someone else. Each campaign has a beneficiary. That beneficiary is not always the person who receives the funds raised. When the client did all of this via human interaction and check writing, it wasn’t much of a problem. They had people behind each interaction. However, they wanted to make sure that there was a delineation between the beneficiary and the recipient, who could also be the organizer. They also needed a next of kin added in the event that the beneficiary / recipient could not accept the funds for one reason or another, and this all needed to be abundantly clear to the campaign organizer on the front end of the experience. So how did we visually represent this? ![](https://cms.echobind.com/assets/204ed6b8-c02b-4b64-9a48-1a3c8a38a403) Organizing the information this way allows everyone involved to point to representations of the individuals rather than needing to use names that can get complicated quickly. When we first started to identify the individuals we said things like, “Okay let’s assume that Officer Santiago wanted to create a campaign for Sergeant Jones, but Sergeant Jones passed and we need to make sure that his spouse receives the money raised in the campaign…” While this first sentence is not terribly complicated, it quickly becomes complicated when you have to keep remembering names to keep individuals straight. By using ducks, dogs, cows, and bears, you make remembering each individual significantly easier, and you spend more time thinking about the core concept and the way it might need to be represented on the front end. On top of making discussions easier, it makes future knowledge transfers easy as well. If I show this to a designer later and talk through it, they will be able to easily understand the various relationships that need to be thought about to design an intuitive experience. ### Conclusion All of the visualization tools we use in a meeting or when presenting to others can be amazing when it comes to complex conversations and concepts. However, we need to be careful to not introduce noise into the process and make having these conversations more difficult. The more of our brain power that goes into discussing the core concept rather than all of the unimportant details that surround the core concept, the better. One word of warning is to make sure that when you start to hack and slash at the noise, you don't also cut out important details that aid in understanding. That can be just as detrimental as having too much noise. *Work with Zack and all of his talented visuals by [reaching out to us via our contact form](https://www.echobind.com/contact), or don't hesitate to write us an email at hi@echobind.com* --- [View on echobind.com](https://echobind.com/post/noise-reduction-in-core-concept-conversations) --- # The Undeniable Power of Microinteractions > Microinteractions are highly prioritized at Echobind both in design and in development. Software needs to connect with its users at a deeper level. _By Eloisa Docton · 2024-03-01_ Today, over half of all businesses have apps, offering extensive reach to connect with their customers. It's crucial to ensure that these connections are as meaningful and helpful as a client contacting someone within your business. At Echobind, we prioritize the end-user in everything we do. Our process focuses on identifying the best path towards your business goals by solving problems for your users, often leveraging microinteractions to achieve this. And in our minds, these are *impossible* to just skip over. ![](https://cms.echobind.com/assets/3fbf414a-a159-43ae-a701-5e41c89f9c73) *Credit: Abdul Latif* ## Microinteractions are everywhere, all the time If you use apps like Amazon and Uber, you have experienced well-thought-out microinteractions, even if you didn't realize it. Microinteractions are the small, subtle moments within a user interface or product experience that contribute to overall user satisfaction. These interactions are brief, focused, and task-oriented, providing feedback, communicating status, or assisting users in specific actions. From a design standpoint, microinteractions are crafted to enhance usability, engagement, and satisfaction. They encompass animations, sounds, visual changes, or other dynamic elements that respond to user input or system events. These details significantly impact user perception and interaction with a product or interface. ![](https://cms.echobind.com/assets/70bce39c-ce83-45b8-ab26-1dafbfbf1355) *Credit: sitiotecno* I like to compare the experience to the difference between using a GPS vs a paper map. With technology, you can get to your location faster, and probably safer because you can keep your attention on the road straight ahead. Studies suggest that microinteractions can increase user interaction by 30%, contributing to enhanced engagement and interaction metrics. Here's how: - **Enhanced User Experience (UX):** Microinteractions provide instant feedback, clear visual cues, and subtle animations, guiding users through the app interface for a seamless and enjoyable experience. ![](https://cms.echobind.com/assets/4575ae53-3c9f-4238-915a-b6b39a2ca770) *Credit: alifia* - **Increased User Engagement:** Users feel more engaged when encountering microinteractions that respond to their actions, encouraging exploration and interaction with the app. ![](https://cms.echobind.com/assets/83397eb4-2349-4241-81f0-d680018e754e) *Credit: Sergei Tarasenko* - **Improved Usability:** Well-designed microinteractions assist users in completing tasks efficiently, confirming selections, indicating progress, or providing error messages, thus enhancing usability and user-friendliness. ![](https://cms.echobind.com/assets/9f4f8bea-c28c-448e-917b-9fa0f0ce4a6e) *Credit: Tre Ishak* - **Emotional Connection:** Microinteractions evoke positive emotions and delight among users, creating memorable experiences that foster an emotional connection with the app. It makes them want to use it more and continue to open it again and again. ![](https://cms.echobind.com/assets/3966511b-0fd4-4591-8997-70fe0dd306d0) *Credit: Dev Ashish Deval* - **Faster Task Completion:** Microinteractions provide instant feedback, aiding users in navigating the app efficiently, leading to increased productivity and satisfaction. ![](https://cms.echobind.com/assets/0a15e208-69e3-4494-95b9-5366e1f5a790) *Credit: KimTomasik* https://echobind.com/post/the-undeniable-power-of-microinteractions Apps prioritizing user experience through thoughtful microinteractions retain users better and foster long-term loyalty. ![](https://cms.echobind.com/assets/e78506c2-4946-4ff5-a683-835463e7fff4) *Credit: John Boe* - **The Main Takeaway:** Investing in well-designed microinteractions enhances app success, creating meaningful and delightful experiences for users. ![](https://cms.echobind.com/assets/ee0c7101-e920-4ff7-86d2-a4551b718c88) *Credit: Abdul Latif* ## **We pride ourselves on this work** Let us design the best experiences imaginable for you and your technology. Like we said, we want users of your software to feel like it was specifically created for them. Easy to use and just an overall joy to tap or click around. Don’t hesitate to [reach out via our contact form](https://www.echobind.com/contact), or fire off an email to us at hi@echobind.com. Happy microinteraction-ing! --- [View on echobind.com](https://echobind.com/post/the-undeniable-power-of-microinteractions) --- # Deep Dive Into Extending tsconfig.json > Here's how to get your typescript code to conform to the same settings throughout the project, from senior software engineer Cully Larson. _By Cully Larson · 2024-02-27_ You have a monorepo with a client app, an API, and some shared packages. You want your typescript code to conform to the same settings throughout the project. The easiest way to do that is to create a `tsconfig.json` file in the root project folder and then have each package extend it. **packages/tsconfig.json** ```json { "compilerOptions": { "strict": true, "moduleResolution": "Node16", "module": "Node16" } } ``` **package/tools/tsconfig.json** ```json { "extends": "../tsconfig.json", "compilerOptions": { "baseUrl": "./", "rootDir": "./src", "outDir": "dist" } "exclude": ["./dist", "./node_modules"] } ``` This works great. However, you notice that all of your packages use the same `baseUrl`, `rootDir`, `outDir`, and `exclude`. So why not just put those in the base `tsconfig.json`? **packages/tsconfig.json** ```json { "compilerOptions": { "strict": true, "moduleResolution": "Node16", "module": "Node16", "baseUrl": "./", "rootDir": "./src", "outDir": "dist" }, "exclude": ["./dist", "./node_modules"] } ``` **package/tools/tsconfig.json** ```json { "extends": "../tsconfig.json" } ``` The problem is the paths in a `tsconfig.json` are relative to the folder that that specific `tsconfig.json` file is in, and not the `tsconfig.json` doing the extending. So in this example, all the paths in `packages/tsconfig.json` are relative to the `packages/` folder, even when `packages/tsconfig.json` is extended (i.e. extending doesn’t change the folder that paths are relative to). And it doesn’t matter if you put `./` in front of the path or not. So, in our last example above, if we run `cd packages/tools && tsc --showConfig`, we’ll get: ```json { "compilerOptions": { "strict": true, "moduleResolution": "node16", "module": "node16", "baseUrl": "..", "rootDir": "../src", "outDir": "../dist" }, "files": [ "./src/tool.ts" ], "exclude": [ ".././dist", ".././node_modules" ] } ``` See how the paths all point to the parent folder? They are not relative to the `packages/tools` folder. That means we always have to define `baseUrl`, `rootDir`, `outDir`, `include`, and `exclude` in the child `tsconfig.json` (the one that extends the base `tsconfig.json`). ## There’s more to `exclude` That last sentence isn’t the whole story for `exclude`. You can actually define `exclude` in the base `tsconfig.json` if you use wildcards. To illustrate this, let’s look at an example with `allowJs: true` to highlight something: **packages/tsconfig.json** ```json { "compilerOptions": { "strict": true, "allowJs": true, "moduleResolution": "Node16", "module": "Node16" }, "exclude": ["./dist", "./node_modules"] } ``` **package/tools/tsconfig.json** ```json { "extends": "../tsconfig.json", "compilerOptions": { "baseUrl": "./", "rootDir": "./", "outDir": "dist" } } ``` Build the `tools` project (`tsc --build`), and then run `tsc --showConfig`: ```json { "compilerOptions": { "strict": true, "allowJs": true, "moduleResolution": "node16", "module": "node16", "baseUrl": "./", "rootDir": "./src", "outDir": "./dist" }, "files": [ "./dist/src/tool.js", "./src/tool.ts" ], "exclude": [ "../dist", "../node_modules" ] } ``` See that `files` includes `dist/src/tool.js`? That file is not being excluded. However, if we use wildcards in the `exclude` property in the base config: **packages/tsconfig.json** ```json { "compilerOptions": { "strict": true, "allowJs": true, "moduleResolution": "Node16", "module": "Node16" }, "exclude": ["**/dist", "**/node_modules"] } ``` And run `tsc --showConfig` again: ```json { "compilerOptions": { "strict": true, "allowJs": true, "moduleResolution": "node16", "module": "node16", "baseUrl": "./", "rootDir": "./src", "outDir": "./dist" }, "files": [ "./src/tool.ts" ], "exclude": [ "../**/dist", "../**/node_modules" ] } ``` Notice that `dist/src/tool.js` is no longer included. This is because the wildcard `**/dist` matches `packages/tools/dist` too, even though it’s relative to the `packages/` folder. That means you can define `exclude` in the base config and have it affect the extending packages. You just have to use wildcards. You might still opt not to define `exclude` in the base `tsconfig.json` and force each package to define its own since will avoid potential issues if someone forgets a wildcard or a package has other folders that need to be excluded. On a side note, you could use a single star (e.g. `*/dist`) too and it will work the same way. ## What about `include`? `include` works the same way as `exclude`. However, the wildcard solution does not work in the base `tsconfig.json`. You’d think it wouldn’t work because it will match all the files in any folder named `src` in the project, but it’s weirder than that. For example, let’s say you have another project in `packages/api` with a file named `src/handler.ts`. If you then run `cd packages/client && tsc --showConfig`, you’ll get: ```json { "compilerOptions": { "strict": true, "allowJs": true, "moduleResolution": "node16", "module": "node16", "baseUrl": "./", "rootDir": "./src", "outDir": "./dist" }, "files": [ "../api/src/handler.ts" ], "include": [ "../**/src" ], "exclude": [ "dist" ] } ``` Your `client/src/tool.ts` file isn’t even in the list. That’s because `tsc` matches the first folder it finds named `src` and doesn’t keep looking. The `packages/api/src` folder is searched first and `tsc` doesn’t continue on to look at `packages/client/src`. So you will have to define `include` in each of the child `tsconfig.json` files. ## Some other things It’s worth noting that when you extend a `tsconfig.json`, the properties in `compilerOptions` are merged. And when both files define the same property, the child `tsconfig.json` wins. However, this is not the case with `include` and `exclude`. If you define them in the child `tsconfig.json`, that exact value will be used; it won’t be merged with the value from the base `tsconfig.json`. We used a monorepo for these examples, but you could run into this situation in a single-project repo as well. For example, if you want to override `tsconfig.json` in a `tests/` folder or a folder with Storybook stories. *Cully is a senior software engineer who you can [follow on X here](https://twitter.com/cullylarson). Feel free to reach out to us for any and all of your technology-building needs at hi@echobind.com. Or hit our [contact page](https://echobind.com/contact).* --- [View on echobind.com](https://echobind.com/post/deep-dive-into-extending-tsconfig-json) --- # The Unseen Value of Completing Side Projects > We've all got side projects as software engineers. And they range in topic and intensity. Here's how Dennis Campos would categorize them. _By Dennis Campos · 2024-02-22_ We all have them - the graveyards of side projects. These ventures linger in the shadows, often half-finished or untouched, reminiscent of relics in a forgotten tomb. Yet, within these endeavors lie hidden treasures, often overlooked and undervalued. This is particularly true for developers, though the insights here resonate across various professions. ## Provides diversification Side projects serve as a sandbox for developing skills. They provide engineers with a unique platform to experiment with new technologies, methodologies, and concepts that they may or may not encounter in their day-to-day role. The exploration is not about learning a language or tool but about embracing a holistic approach to problem-solving, and creativity. You will adapt to more patterns and make connections to your work. ## Fulfillment and Creative Outlet Aside from professional benefits, side projects can serve as a creative outlet and provide a sense of personal achievement and fulfillment. They allow engineers to explore areas they are passionate about, independent of the constraints typically found in their primary job. Side projects also create space for self-expression, enabling engineers to bring their unique ideas to life, experiment without fear of failure, and cultivate a sense of pride in their individual accomplishments. ## Industry Trends The tech industry is constantly evolving, making it challenging to keep up with current trends. Side projects provide a practical way to stay up-to-date with emerging technologies, ensuring that we remain relevant in our field. Showing other developers that you are actively involved in your craft, open to challenges, and constantly seeking knowledge can create a positive impression. This can foster collaboration and provide opportunities for learning. ## Conclusion So, continue working on those side projects, even if you end up abandoning them in your GitHub graveyard. I leave you with this [commitstrip](https://www.commitstrip.com/en/2014/11/25/west-side-project-story/?) image that captures the outlook of side projects. ![](https://cms.echobind.com/assets/ba84e0b8-11af-4ede-9d18-1273aa1e24a9) Ready to bring your vision to life, no matter the scale? Let's collaborate to turn your ideas into reality. [Get in touch with us today](https://echobind.com/contact). --- [View on echobind.com](https://echobind.com/post/the-unseen-value-of-completing-side-projects) --- # Why Video Has Always Been The Key > Content creator Sean Ely dives into his history and love of videography as well as why he's so thrilled to be doing marketing for you at Echobind. _By Sean Ely · 2024-02-20_ Since 2009, all I’ve ever been excited to do is make videos. I borrowed a friend’s camcorder and somehow won a [nationwide basketball trick shot contest](https://youtu.be/tPBNaJW9PXU?si=1JpkvEC_ucDWAhM0) with my college roommate during our senior year, where I shot and edited a montage of us making ridiculous baskets that we scripted. From that moment on, I was hooked. Learning how to cut scenes together from trial and error in Windows Movie Maker was a high I’d never experienced before. ![](https://cms.echobind.com/assets/80cc99ae-ee32-406f-9b52-25914c8e3df7) But for the last four years leading up to that day, I had been studying to be a print journalist. Not a video editor. I hustled down leads and treated the Associated Press Stylebook like the Bible. I interned in small newsrooms, freelanced at magazines and obsessed over my writing classes like Mozart treated sheet music. How did a few late nights of editing suddenly have me wanting to change the entire direction of my future? With videography, of course. Not basketball. I was a 5-foot-nothing kid who spent the semester eating Pasta Roni every meal. I wasn’t exactly “league ready” despite the grand prize giant check that arrived to my apartment. ![](https://cms.echobind.com/assets/86bdb816-a564-490c-a13a-894925552038) So I thought to myself, “What if I could combine video editing ***AND*** reporting into the perfect job?” Upon graduation two months later, I clung to the field of “multimedia”, where I could meet people and tell their stories as a reporter both in writing, as well as on camera and in the editing room, putting it all together on newspapers’ websites as one complete package. And for years, that’s what I did. At the root of it all, I realized why this “work” was so intriguing to me: Narrative. No matter who or what I was interested in, I wanted to properly tell their story in an easily-digestible way, with a proper beginning, middle and end. I became addicted to taking a topic that not everyone was familiar with, and making it the most intriguing thing in their lives that day. And a few years later, I added a third love to the mix: Marketing. ![](https://cms.echobind.com/assets/4729bbf9-9c0c-4ba0-8f51-95bef76b819c) A friend passed along a job posting where a startup marketing company was paying four people across the country to wear a different company’s logo on their T-shirts every day of the week, and make a video that marketed their brand in a way never seen before. Write a script, film and edit the video, and upload it to the internet. Every day, for years. Uh, did someone create this opportunity specifically for me? The research and writing? Journalism. The cameras and editing software? Videography. The unique brand awareness and call-to-action mindset? Marketing. This is exactly what I was meant to do in my career. [So I made a video resume to stand out from everyone](https://youtu.be/6PSIRB5w-rk) and got the job. ## Fast forward a decade, and here we are today at Echobind After having freelanced with Echobind for years—[helping this amazing agency tell their story](https://vimeo.com/774491346?share=copy)—I was fortunate enough to come aboard full time to do my favorite type of work every day. Making videos and marketing strategies for companies that believe in technology and staying ahead of their competition…while telling *their* unique stories? Of course. I get to research innovative brands top to bottom and provide my thoughts on how they can flourish best in their landscape. And it's work I'm ecstatic to provide *your* company if you work with us, alongside beginning to create informative, high-energy videos that summarize what you do best. It can live on your homepage, be pinned to the top of your socials or work as an email marketing asset to show everyone what you’ve built. It accents your technology and overall design work beautifully. **Remember**: There’s nothing like well-done visual communication. Whether it’s a studio movie or an eight-second TikTok, we are more enamored by video in 2024 than any other consumable medium. Bringing together my love of video alongside focusing so passionately on your storyline and what makes you special is something I cannot wait to dive into with you. ![](https://cms.echobind.com/assets/7444238c-af95-4ce9-bc82-d9b39a56430f) ## A whopping 91% of people have watched an “explainer video” to better learn about a product or company And 69% of consumers believe a product demo best assists them when making a purchase. Yet, only 47% of marketers have invested in these videos despite 88% of marketers “believing it’s important to their strategy.” Since I've arrived at Echobind, we can now prioritize video content more than ever before. I’m ecstatic to work this year alongside Echobind's wildly-talented engineers, product specialists and designers to give our clients the most well-rounded set of software imaginable, from how it looks to how it works. If you're passionate about video and want to chat more, visit our [contact page](https://echobind.com/contact). --- [View on echobind.com](https://echobind.com/post/why-video-has-always-been-the-key) --- # NativeWind for Universal Styling in Expo > NativeWind merges Tailwind CSS with React Native, streamlining dev for web & mobile. It features universal styling, precompiled styles, and enhances the developer experience with Expo. _By Isaiah Grey · 2024-02-15_ In the quest for a unified and efficient development workflow across web and mobile platforms, NativeWind emerges as a transformative solution. By harnessing the power of Tailwind CSS, NativeWind offers React Native developers a universal style system that's precompiled, fast, and feature-rich. This blog post explores how NativeWind enables consistent styling experiences across platforms, enhances developer experience, and ensures code maintainability within the Expo framework. ### The Power of NativeWind in Cross-Platform Development NativeWind's philosophy centers on providing a consistent styling experience, irrespective of the platform. It achieves this through several key features: - **Universal Styling:** Adapts the best style system for each platform, ensuring your components look and perform optimally whether on the web or native. - **Precompiled Styles:** Leverages Tailwind CSS's compiler to generate styles at build time, minimizing runtime overhead. - **Fast Runtime:** A minimal runtime efficiently applies responsive styles without compromising performance. - **Developer Experience:** Offers plugins for easy setup and enhanced intellisense support, improving the development workflow. - **Rich Feature Set:** Supports dark mode, arbitrary classes, media queries, themes, custom values, and plugins, along with pseudo-classes like hover, focus, and active. ### Implementing Universal Styling with NativeWind Let's demonstrate how to build a responsive, styled component using NativeWind within an Expo project. We'll focus on creating a component that benefits from NativeWind's universal style system, including responsive and UI state styles. #### Setting Up Your Expo and NativeWind Environment Ensure your development environment is set up with Expo and NativeWind. First, create a new Expo project if you haven't already: ```bash expo init MyUniversalApp cd MyUniversalApp ``` Then, follow NativeWind's installation instructions to integrate it into your project. This may involve adding NativeWind and its Babel plugin to your project dependencies and configuring your Babel setup to use NativeWind's plugin for style precompilation. Follow the instructions in the [NativeWind documentation](https://www.nativewind.dev/quick-starts/expo) to set up NativeWind in your Expo project. #### Creating a Responsive Component with NativeWind With NativeWind, you write your components using Tailwind CSS classes directly in React Native code. NativeWind's Babel plugin and build-time processing ensure these styles are efficiently applied across platforms. ```jsx import React from "react"; import { Text, View } from "react-native"; const ResponsiveCard = () => { return ( Responsive Styling with NativeWind ); }; export default ResponsiveCard; ``` This component utilizes Tailwind's utility classes for styling, including responsive and dark mode variants. NativeWind ensures these styles are correctly applied whether your app runs on the web or a native platform. ### Best Practices for Using NativeWind in Expo Projects - **Leverage Precompiled Styles:** Maximize build-time style generation to reduce runtime overhead and improve app performance. - **Utilize Rich Styling Features:** Explore and use NativeWind's support for pseudo-classes, responsive design, and device state styles to create sophisticated UIs. - **Focus on Developer Experience:** Take advantage of NativeWind's development plugins and intellisense support to streamline your workflow. NativeWind represents a significant advancement in cross-platform development, offering a universal style system that brings the best of Tailwind CSS to React Native. By integrating NativeWind into Expo projects, developers can enjoy a consistent, efficient, and feature-rich styling experience across web and mobile platforms. Embrace NativeWind to elevate your cross-platform development to new heights. ### Bonus: Enhancing Cross-Platform Compatibility with `@expo/html-elements` When building components that need to run smoothly across different platforms, including web projects outside of Expo, such as a Next.js app, [@expo/html-elements](https://www.npmjs.com/package/@expo/html-elements) comes as a savior. This package provides a suite of universal semantic HTML elements as React components, optimizing for SEO and accessibility while ensuring an optimal UI across iOS, Android, web, and desktop apps. #### Why Use @expo/html-elements? - **SEO and Accessibility Optimization:** Using `@expo/html-elements` enhances your website's SEO and makes your native apps more accessible to physically impaired users. - **React-Native-Web Compatibility:** It takes full advantage of react-native-web accessibility rules whenever possible. - **Universal Component Rendering:** For example, the `H1` component renders as `

` on the web, a `UILabel` on iOS, and a `TextView` on Android, all while accepting styles from the StyleSheet API. - **TypeScript and Style Support:** Ensures full TypeScript support across platforms and allows using `href` on a Text element without additional patches. #### Setting Up `@expo/html-elements` To start using @expo/html-elements in your project, simply install the package: ```bash yarn add @expo/html-elements ``` Then, import and use it in your components: ```jsx import { H1 } from "@expo/html-elements"; const MyComponent = () =>

This is a header

; // This component will render as an H1 on the web and as a Text component in React Native with the appropriate attributes and styles. // Web =>

This is a header

// Native => This is a header ``` #### Supported Components `@expo/html-elements` supports a wide range of HTML elements, providing a React component for each. Here are some of the supported elements: - **Text Elements:** `H1`, `P`, `B`, `I`, and more, each rendering appropriately across platforms. - **Layout Elements:** `Div`, `Header`, `Footer`, `Section`, `Nav`, and others, adapting to the correct semantic element on the web and View equivalents on native platforms. - **List Elements:** `UL`, `LI`, offering the traditional list presentation. - **Other Elements:** `A` for links, `Table` and its related components for tabular data, ensuring a consistent look and feel across devices. For a full list of supported elements, refer to the [package documentation](https://www.npmjs.com/package/@expo/html-elements). These components automatically adapt their rendering based on the platform, ensuring that you consistently use the correct semantic and accessible component for iOS, Android, and the web. Integrating `@expo/html-elements` into your Expo project not only streamlines the development process across platforms but also ensures that your components are semantically correct, accessible, and SEO-friendly. This package bridges the gap between native and web development, allowing you to focus on crafting excellent user experiences while it handles the nuances of platform-specific rendering. ***Learn more about Echobind's [React Native capabilities](/services/react-native-app-development).*** ### Resources - [Expo Documentation](https://docs.expo.dev/) - [React Native Documentation](https://reactnative.dev/docs/getting-started) - [TailwindCSS](https://tailwindcss.com/) - [NativeWind](https://www.nativewind.dev/) - [Expo HTML Elements](https://www.npmjs.com/package/@expo/html-elements) --- [View on echobind.com](https://echobind.com/post/native-wind-universal-styling-expo) --- # Product Manager and Project Manager Roles Explained > Project managers and product managers work in tandem as a synchronous 1-2 punch to ensure your project's success. They are crucial in what we do at Echobind. _By Claire Surma · 2024-02-13_ At Echobind, we're proud of our track record in creating exceptional web and mobile applications for a diverse range of clients, from nimble startups to Fortune 500 powerhouses. A key factor in our projects' success lies not just in the technical skill of our development and design teams, but also in the strategic and organizational direction of our Product and Project Managers. Yet, we sometimes encounter clients questioning the necessity of these roles in their projects. Let's explore what Product and Project Managers do, and how they work together to ensure project success. ## **Understanding the Roles** ![](https://cms.echobind.com/assets/7a007216-7bc5-4d75-b444-6abb4570febf) ### **Product Manager: The Visionary** Product Managers at Echobind are the visionaries who ensure your project transcends being a mere fix to becoming a lasting solution to core user issues. By deeply understanding market needs, user expectations, and business goals, they set a clear, strategic direction for the product. This involves: - **Defining the Product Vision:** Establishing a clear, strategic vision for the product and ensuring that every feature developed aligns with this vision and the user's needs. This includes identifying success metrics and crafting user flows that serve as the blueprint for your application. - **Prioritization**: Deciding what features to build and in what order, to deliver maximum value to the users while aligning with business objectives. - **Stakeholder Alignment:** Ensuring that all stakeholders, including clients, developers, and designers, are aligned with the product's goals and understand the roadmap. - **User Feedback:** Incorporating user feedback into the development process to ensure the product meets market demands and user expectations. - **User Acceptance Testing:** Managing client user acceptance testing, and ensuring the final product aligns perfectly with the initial vision for a seamless project handoff. ![](https://cms.echobind.com/assets/02c52ffa-a567-478e-96ac-7e05fcadfc88) ### **Project Manager: The Orchestrator** Project Managers are the orchestrators, planning and executing the project to ensure timely delivery within scope and budget. They are your advocates, focusing on logistics, facilitating seamless team coordination, and managing risks so that your project stays on track. As your main point of contact, they ensure efficient resource allocation and clear communication across all project stages. Their duties include: - **Project Planning:** Defining the project's scope, setting timelines, and allocating resources. - **Team Coordination:** Facilitating communication between team members and ensuring that everyone has the information and tools they need to perform their tasks effectively. - **Risk Management:** Identifying potential risks and developing mitigation strategies to keep the project on track with timelines and budget. - **Quality Assurance:** Monitoring project feature development progress and making adjustments as needed to ensure the final product meets quality standards. ## **Why Both Roles Are Essential for Success** While their focus areas differ, both Product Managers and Project Managers are essential for the success of a software development project. Here's why: - **Strategic Alignment and Execution:** Product Managers ensure that the product being developed aligns with business goals and user needs, while Project Managers ensure that the project is executed efficiently and effectively. Bottom line, they help you build a better product. - **Risk Reduction:** Sometimes clients are too involved in the nitty gritty details of a product to see the larger issues. Having both roles collaborate on a project helps identify and address risks from both a product and project standpoint, reducing potential project pitfalls. - **Resource Optimization:** Product Managers and Project Managers work together to ensure that resources, including time, budget, and team skills, are allocated optimally to meet project objectives. Ultimately, this saves clients money, especially when a launch deadline is important. - **Enhanced Communication:** These roles serve as vital communication hubs, ensuring that all stakeholders are informed and aligned throughout the project lifecycle. Our strategy team's involvement in projects is not just an added service; it's a fundamental part of project success. The detailed involvement of our product and project managers in every phase of the project, from initial discovery workshops to post-launch retrospectives, ensures improved communication, efficiency, and a final product that truly delivers on its promise. *To work with a dedicated strategy team on a project right here at Echobind, don’t hesitate to reach out to us at hi@echobind.com. And get ready to have all your ducks in a row to make the best product possible.* --- [View on echobind.com](https://echobind.com/post/product-manager-and-project-manager-roles-explained) --- # 5 Minute Redesigns: Improving Hierarchy in 2 Steps > Efficiency at Echobind is a staple of our business, especially when it comes to incorporating smart design into our development work. _By Kaila VanSumer · 2024-02-05_ At Echobind, we place a lot of importance on efficiency. This often means we’re trying to accomplish *a lot* with minimal resources. This mindset has a way of hyper-focusing our attention on the things that matter most so we can maximize the time and money we invest in projects. This can be witnessed frequently in the collaboration between our engineering and design teams. Sometimes, projects are staffed with engineers but not with a dedicated designer. In these cases, our engineers will sometimes ask a designer to make suggestions about small improvements they can make to a screen without having to invest a lot of time. Recently one of our engineers dropped this screen into our Slack and asked for a quick 80/20 analysis and design recommendations on how to improve it. ![](https://cms.echobind.com/assets/a0725809-3950-4d38-ad83-c77fdcb3ce5c) The biggest problem with this screen is that there are three main elements that feel like they have equal amounts of visual weight even though they’re not all equal in importance. ![](https://cms.echobind.com/assets/b2a6e244-0123-4519-9323-a7c58da57513) This is a common challenge when designing UI, and for a screen like this it can be tackled with 2 simple steps: 1. Assign a priority to each element 2. Use ✨Design Magic✨ to give the right amount of visual weight to each element based on importance When we’re finished, the screen should feel balanced, and the most important elements should attract the most attention while the least important should be present but not distract from the main thing. ## Step 1: Assign a Priority to Each Element First, let’s assign a priority number to each of the 3 elements we identified previously. ![](https://cms.echobind.com/assets/3c41ab73-2f13-477b-93db-7969ef4ab41a) For this screen, the most important element is the “Scan Barcode” section since that’s the main action most users are going to want to take on this screen. The second is the “Scan a barcode to get started” section, which is actually a way for the user to manually enter a barcode number if they’re unable to scan the barcode for some reason. The least important element is the welcome message and number of orders completed. The orders completed is a nice number to know, but it doesn’t need to take center-stage; it just needs to be somewhere on the screen where the user can find it if they need it. ## Step 2: Adjust Visual Weight Based on Priority Now that we’ve identified what’s most important, let’s use some basic design principles to make adjustments to each element. **Remove the Least Important Element from Center Stage** The [Interaction Design Foundation](https://www.interaction-design.org/literature/topics/center-stage#:~:text=Center%20stage%20is%20a%20design,important%20when%20carrying%20out%20tasks.) defines center stage as ”a design pattern where the most important information, panel, window, or toolset appears prominently at the center of the user interface.” So one way we can de-emphasize the least important element is by removing it from the center. Let’s put the welcome message and the orders completed text in a row at the top. We’ll also further de-emphasize the orders completed text by removing the container border, decreasing the font size, and making it lighter gray in color. ![](https://cms.echobind.com/assets/d342dcdf-4e6e-4284-9438-73f12f1e2a06) **Make the Most Important Element Stand Out** As it is, this design already does a decent job of making the “Scan Barcode” section stand out, but it’s still competing a lot with that chunky input and “Submit” button. Additionally, the microcopy doesn’t give the user any information about *why* they should scan a barcode. We’re going to fix this using a couple different techniques. First, we can update the copy and add some secondary text that gives the user more context. We’ll make sure the header is large, dark, and bold while keeping the secondary text smaller, lighter, and regular weight. Next, we can put the “Scan Barcode” section in a white container and give it a subtle drop shadow to make it stand out from the background. We’ll also make the background a light gray color instead of white to further emphasize this separation. ![](https://cms.echobind.com/assets/8480ba51-8bc3-4f03-8438-0f954a09b9eb) Nice! This is already looking much better. But as I mentioned before, that input and “Submit” button are still taking up quite a bit of space and competing with the main section. **Compare Elements of Medium-Priority to Other Elements** The biggest problem with the input and “Submit” button is that they’re too large compared to everything else on the screen. The help text is also too dark and doesn’t let the user know that the input is an alternative method to scanning, so we’ll fix that while we’re at it. By updating the copy, making the text lighter, and decreasing the size of the input, now everything feels *just* right. As a side note, placeholder text can be problematic for a number of reasons including not being readable by some screen readers, so I always recommend using a label and ditching placeholder text altogether. Adam Silver has a great [blog post](https://adamsilver.io/blog/the-problem-with-placeholders-and-what-to-do-instead/) on this if you’re interested in learning more. ![](https://cms.echobind.com/assets/f8c5cfde-8423-4736-94db-387e880ff689) So there you have it! This redesign took me about five minutes, didn’t cost the engineer much time to implement, and had a big impact on the overall usability of the product. --- [View on echobind.com](https://echobind.com/post/5-minute-redesigns-improving-hierarchy-in-2-steps) --- # Lex Is Ready To Chat > Account manager Lex Meola has returned from maternity leave and loading up her schedule to help take companies to the next level. _By Echobind Staff · 2024-01-31_ Before we could even finish clapping for her, the incomparable Lex Meola was already diving right back into her workload. No surprises there whatsoever. A huge congrats to our account manager who is officially back from maternity leave after the birth of her ridiculously-adorable daughter Aria. Don’t worry, we’re way ahead of you: Yes, she’s got pictures to share. Don't hesitate to ask. ![](https://cms.echobind.com/assets/18ba2ed5-517a-4c7f-8ee1-dc4ef782f053) Lex is queen bee around here when it comes to hiring and working with Echobind this year, and her schedule is now officially open for business. [Grab time with her](https://savvycal.com/lexmeola/dcf1e6ba) to chat about one (or all) of the following: - How to create a digital strategy or accelerate your plans - Dive deep into creative design work - Integrate a new web application alongside your website - ...or maybe you just want someone to hash out which trends your company should consider paying attention to this year in order to stand out Lex is your gal. She’s happy to chat about how we can work together to push your technology further this year. Feel free to [snag a time slot on her calendar](https://savvycal.com/lexmeola/dcf1e6ba), or email her directly at lex at echobind.com. We [can’t wait to chat with you](https://echobind.com/contact). *Team Echobind* --- [View on echobind.com](https://echobind.com/post/lex-is-ready-to-talk-to-you) --- # Tips for dealing with time > There are lots of ways to make mistakes when working with dates and times. Here are some quick tips to avoid those footguns. _By Cully Larson · 2024-01-16_ It’s happened. You have to write some code that uses dates and times. You head to the bathroom to have a little cry in private only to realize a coworker was in there the whole time and now you have to explain yourself. Sorry, I lost my train of thought. You’re back from the bathroom, your coworker is back-channeling stories about your little cry, and you’re about to start writing date/time code. If this is your first rodeo, or your second rodeo, or really it doesn’t matter how much rodeo experience you have, footguns await you (metaphorical footguns unless your code is being used in some kind of self-firing gun which is not a good idea, please stop working on that project). Here are some tips to help you avoid a few of those footguns. You’re still going to shoot yourself in the foot (again, likely metaphorically) because of timezones and daylight saving time, but hopefully these tips will reduce the number of shots to those precious piggies. ## Include units in your variable names You have some variables that will hold periods of time. Maybe `timeout`, `retryTime`, `activationDelay`, etc. When you write the code, you’ll know what units of time those variables hold (e.g. seconds, milliseconds, days). But the next dev to come along, or your future self, won’t know. You’ll have to track down the code where the value is eventually used and hope that it gives some indication of the units. But it isn’t always that clear and either way it’s a waste of time tracking down that logic. To avoid this, you should always include the units of time in your variable names. For example, `timeoutMs`, `timeoutSeconds`, `retryTimeMinutes`, `activationDelayDays`. That way it’s always clear what units of time those variables hold. You may also be able to use something like branded types to guarantee units. Though that can add some complexity to your app that might not be necessary if you use good naming conventions. ## This is good for timestamps too It’s worth noting that including units in your variable names is also very helpful with timestamps. Since milliseconds and seconds are both used for timestamps, it can be challenging to know which is used in a particular variable. Including the units in your timestamp variables names (even in your database field names) will clear up this ambiguity. For example, `createAtMs`, `modifiedAtSeconds`, etc. ## Include conversions in comments You have some magic numbers in your code; something like: ```jsx const activationDelaySeconds = 10800; ``` But how long is 10,800 seconds? Well, let’s leave a comment: ```jsx const activationDelaySeconds = 10800; // 3 hours ``` You’ve probably seen comments like this before. The problem is, what if someone comes along and changes the value but forgets to change the comment: ```jsx const activationDelaySeconds = 14400; // 3 hours ``` Unless you can recognize that 14,400 seconds is not 3 hours, you’ll never know that the comment is wrong. Whoever looks at this code will think the activation delay is 3 hours and very likely waste time figuring out why activation isn’t happening at the expected time. Instead of just commenting the value with an easier-to-read format, include the entire conversion in the comment: ```jsx const activationDelaySeconds = 10800; // 10800 seconds = 3 hours ``` That way if someone comes along and changes the value without changing the comment: ```jsx const activationDelaySeconds = 14400; // 10800 seconds = 3 hours ``` It will be clear to whoever looks at it that 14,400 seconds is not 3 hours. It will also make it harder for the person who made the original change to forget to change the comment. Some people use multiplication to make the value more clear: ```jsx const activationDelaySeconds = 3 * 60 * 60; ``` This is more clear than a magic number, but it’s slightly harder to scan; you have to pause and perform the math at least a bit before understanding what the value is. It’s also error-prone. It’s easy to mess up the math and hard to notice it’s messed up. I would still leave a comment when using this method to indicate intent. ## Use variables for magic numbers Another option for documenting magic numbers is to do something like this: ```jsx const threeHoursInSeconds = 3 * 60 * 60; const activationDelaySeconds = threeHoursInSeconds; ``` This is self-documenting and it’s unlikely someone will change the value of `threeHoursInSeconds` directly because then it won’t be “three hours.” This isn’t necessarily better than leaving conversion comments. It’s just another option. ## Use UTC until you can’t Working with timezones opens up an exotic variety of opportunities to make mistakes. The easiest way to narrow the surface area for those problems is to not deal with timezones until you absolutely have to. A simple way to do this is to keep your dates in the UTC timezone unless you’re displaying information to the user. At that point, convert it to the user’s local timezone. This can be a challenge in Javascript because the input and output of the Date object are usually in the “local” timezone. To get around this, it’s often helpful to prefer timestamps over Date objects. Timestamps also have the benefit of being easily serialized and used by other programming languages. ## Remember daylight saving time If you’re ever working with periods of time (comparing dates, scheduling events, etc), remember that daylight saving time exists. If your date comparison is over a long enough period, you might lose or gain an hour to DST. If you’re scheduling an event, there’s a chance the event could happen twice if DST falls back an hour, or never happen if DST springs forward an hour. If you’re writing a unit test that deals with time, it might pass in October and fail in December because of DST. DST does not start and end on the same dates each year. DST starts on the second Sunday in March at 2am and ends on the first Sunday in November at 2am. Remember that since the clock moves forward by an hour at the beginning of DST, the times between 2:00am and 2:59am never happen on that day (2am becomes 3am immediately). And because the clock moves back an hour at the end of DST, the times between 1:00am and 1:59am happen twice on that day (2am becomes 1am). You may be able to completely avoid these issues by using dates/times in the UTC timezone. ## Be careful around boundaries If you’re working with a date/time that might end up close to the boundary between one day and the next, be careful. It’s easy to be off by one hour, minute, second, and end up on the “wrong” side of that boundary. The problem with bugs like this is you might not notice it unless you’re testing at the exact right time. A couple of ways to help prevent issues around boundaries is to include unit tests for those boundary times, or to simply think, “How will this work if it runs at 23:59, just before midnight? Or at 0:01, just after midnight?” ## Conclusion As we part company in this tenuously together journey, remember that you will shoot yourself in the foot around dates and times (please don’t build that self-firing gun), and in those moments of suffering it will be difficult to remember that you will have shot yourself in the foot far fewer times with all this togetherly garnered knowledge and so put all your effort into remembering because therein lies some simple hope for a bright side and what better thing to shove in the face of your gossipy coworker (a real Janet, a real Chad) than a bloody foot that very well could have been bloodier. --- [View on echobind.com](https://echobind.com/post/tips-for-dealing-with-time) --- # Elevate Your Remote Work Setup With These Staff Picks > The Echobind staff shares their favorite, unique work-from-home items that help workday efficiency and put them in the best headspace every single day. _By Sean Ely · 2023-12-12_ Since Echobind's inception, we've worked hard to gather the best people around. From software engineers, to project managers to product strategists, we cling to individuals who love quality work, push to challenge themselves to learn more and at the end of the day, are just [solid human beings](https://echobind.com/about) at their core. The take 'em home to meet the parents kinda thing, ya know? Not that you would, that would be *extremely* random, but the sentiment remains. You could if you wanted, they're THAT well-rounded. Sorry, let's move on. Also since our company's launch in 2015, we've been fully remote. The work-from-home craze? Uh, yeah, that's less of a craze and more of just another Monday. Which means, we know a thing or two about efficient, yet cozy home office setups. I pinged our staff to pass along their favorite items that help make their individual workdays more enjoyable. Whether it's a piece of technology to level up their projects or even a blanket that keeps them cozy throughout the day, I wanted to hear about it all. With the holidays coming up, there's no better time to have this list handy. You can buy for someone, or just snag an item for yourself. You earned it. ### The Echobind Staff Recommendations List: ![Steelcase leap v2 chair](https://cms.echobind.com/assets/4552009e-7bc0-49f7-91f2-10f8294f8920) **The [Steelcase Leap V2](https://www.btod.com/steelcase-leap-v2) Office Chair**: *Pressure-free seat foam, customizable tilt and arm adjusters, wildly comfortable.* Cully Larson and Brandon Richey agreed this is "the best office chair" they've used in their careers. Cully even said, "I had some lower back pain that basically went away after getting this chair." It's currently 50% off on [btod.com's website](https://www.btod.com/steelcase-leap-v2), which is professionally refurbished to alleviate the pricetag. ![](https://cms.echobind.com/assets/c1a7546e-84d7-4dae-a098-3f8a90a272e6) **Kinesis [Freestyle Edge RGB](https://gaming.kinesis-ergo.com/product/freestyle-edge/) Gaming Split Mechanical Keyboard**: *20-inch adjustable linking cable, one-handed gaming and ergonomic typing, RGB backlighting* Matt Thompson said it "eases my shoulder, wrist and (overall) developer hunch." And for those who want a more modest version and smaller pricetag, the [Freestyle2 version](https://www.amazon.com/Freestyle2-Ergonomic-Keyboard-Standard-Separation/dp/B00CMALD3E/ref=asc_df_B00CMALD3E/?tag=hyprod-20&linkCode=df0&hvadid=309776868400&hvpos=&hvnetw=g&hvrand=14758081964623279940&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9014169&hvtargid=pla-570218617643&mcid=41fdadf5fd8c34ccafa02de263bdc538&gclid=CjwKCAiA98WrBhAYEiwA2WvhOhHvbQ5Nw8pqvvS3eHIuZwwfk5oVYvo77uBvlrQxzS0VbkAjhn4V7xoC4kkQAvD_BwE&th=1) is available for half the price. ![](https://cms.echobind.com/assets/82565842-a0ff-437a-9451-f5a43a7a7ff1) **Blender Bottle's [Koda Water Jug](https://www.blenderbottle.com/products/koda?variant=31536931045443)**: *74-ounce capacity, leakproof lid, carry handle, measurement markings* Alex Anderson swears by this beauty, not only for its design, but the fact he only has to refill it once to hit his daily water quota (because we're all sick of running and forth to the kitchen, let's be honest). And at $14.99, you can't beat the price for the quality of the bottle. ![](https://cms.echobind.com/assets/9d275309-06cb-4937-819c-36aba3e3c179) **[reMarkable 2](https://remarkable.com/store/remarkable-2) Paper Tablet**: *Writes like real paper, converts to a digital file.* It's a digital tablet but it writes like real paper. And then converts it all to typed text. Oh, and the battery lasts two weeks. And it integrates with Google Workspace, too. "I take notes on that and import into Obsidian," said Brandon Richey. ![](https://cms.echobind.com/assets/87447174-0278-4048-8b15-01d19f224c64) **Rigstne [Coffee Mug Warmer](https://www.amazon.com/Rigstne-Coffee-Mug-Warmer-Portable/dp/B0CDQ56N1P/ref=sr_1_5?crid=24HY4JNQTYXBJ&keywords=rhinestone%2Bcoffee%2Bmug%2Bwarmer&qid=1702400826&sprefix=rigstne%2Bcoffee%2Bmug%2Bwarmer%2Caps%2C113&sr=8-5&th=1)**: *Lightning-fast heat-up time, four temperature options, timer function* This thing heats up your beverage in less than 30 seconds and even can be used to help melt candle wax to release aroma in the room. "You just can't go wrong with a good mug warmer," said Brandon Richey. And for $25.99 and 100+ ratings over 4.5/5, we can see why a lot of people love it. ![](https://cms.echobind.com/assets/836d46e4-3aaa-4603-9d48-5d25cc63de8d) **Selk'bag [Original Wearable Sleeping Bag](https://selkbagusa.com/)**: *Synthetic insulation, soft polyester shell, machine washable, crazy warm* Our own Cully Larson loves dropping this one in Slack. People buy one for camping, and then they use it all-year round. Also, if you want to work outside on your patio in the winter, or in an igloo you made with your kids, this comes in handy. Seriously, you could. ![](https://cms.echobind.com/assets/6b5e5b23-3542-4181-9e36-62dd8f06ae9a) **Homemade desk with butcher block slab**: *Sleek, practical, lateral room for days* And finally, we're so excited to show you Mariah Grey's desk setup which truly delivers on both aesthetics and functionality. "I like long deskspace set ups," said Mariah. If you were looking for the motivation to create a new desk setup, this is it going into 2024. So, so clean. Have an item you swear by? [Feel free to fire ideas over to us on X](https://twitter.com/echobind) and keep the conversation going. --- [View on echobind.com](https://echobind.com/post/elevate-your-remote-work-setup-with-these-staff-picks) --- # Make Progress, Even When You’re Stuck > The 15-Minute Rule is relatively straightforward: If you spend 15 minutes (or for the larger scoped problems, longer than estimated) on the same problem without making progress, stop and ask for help. _By Mariah Grey · 2023-11-17_ Here at Echobind, we’re constantly approaching design, strategy, and engineering challenges that take quite a bit of time. Our goal is to work smarter, not harder to solve these issues. We recognize that working *******smarter******* is about finding the most efficient methods and increasing effectiveness. When we do this, we’re better with our time. With that in mind, we live by the 15-minute rule. It’s a simple way for our team to ask for help and recognize the signs that it’s time to loop someone else in. ### What is the 15-minute rule? The 15-Minute Rule is relatively straightforward: If you spend 15 minutes (or for the larger scoped problems, longer than estimated) on the same problem without making progress, stop and ask for help. If help isn’t available, move on to something else and come back to the original problem when you can get another set of eyes. If you google the “15-Minute Rule,” you’ll find a variety of ways people apply it. For some, it’s similar to our approach — essentially preventing us from going down hours-long rabbit holes and spinning our wheels on a problem. For others, it’s to help limit procrastination. The spirit of the rule, however it’s applied, is time boxing. ### Benefits of the 15-minute rule Normally, getting another person to work with you helps reveal a better approach - perhaps one you didn’t know or see initially. Some of the challenges we handle require a lot of time and focus, but the fifteen-minute rule helps us recognize when we are spending *too* much time on a problem. It can help with procrastination. Sometimes, that next ticket might seem daunting, but knowing that you can spend fifteen focused minutes on it before asking for help is the push you need. ### When to follow the 15-minute rule The more time we spend trying to resolve an issue, the more likely we are to become fatigued and start making mistakes. We may become more focused on the problem and less focused on the bigger picture. This leads to less creativity as we become trapped in a narrower way of thinking. Cue the ✨ 15-minute rule ✨. So, how do you know when it is time to ask for help? 1. You have been working on a problem for more time than estimated, with little progress. 2. You feel overwhelmed or blocked and can’t see a way forward. 3. You don’t yet have the experience in this area to solve the problem on your own. 4. The consequences of not solving this problem will impact your project. Knowing it’s the right time to ask for help is important for our professional growth and leads to better problem-solving skills in the future. The next time you find yourself stuck on a problem, reach out for help. --- [View on echobind.com](https://echobind.com/post/make-progress-even-when-you-re-stuck) --- # UX Best Practices for Designing Signup and Login Forms > When you design any app, chances are good you’ll need to design signup and login screens. Here are some best practices for designing signup and login flows. _By Kaila VanSumer · 2023-10-26_ ## Introduction When you design any app, chances are good you’ll need to design signup and login screens. Not only are these screens ubiquitous, they’re also some of the first screens new users will see when using your app, which makes them great places to invest in user experience. Here are a handful of best practices we follow at Echobind when designing signup and login flows for the apps and websites we design and develop. ## Signup ### Ask for as little information as possible The biggest lever you can pull to improve the UX of your signup form is to only ask for information that is essential. It’s tempting to ask users for data that _might_ be useful, but this puts unnecessary burden on the user, erodes trust, and often decreases conversion rates. It helps if you pretend every field in your signup flow must justify its necessity. For each field ask yourself the following questions: - How will these data be used? - By whom? - For what purpose? If you (or your client) can’t come up with a specific and direct answer to all three of these questions for a field, then that field should not be added to the signup flow. ### Show password requirements as help text Most apps will have requirements for passwords for security reasons. This includes things like minimum character counts, capitalization rules, and special character requirements. If your app has password requirements, it’s best to show these requirements to the user when and where they are relevant (i.e. as help text near the password form field). If your app has simple password requirements that can fit on a single line, add that line below the label and above the form field. ![](https://cms.echobind.com/assets/9ee03254-c1f8-47ef-ab3e-7168231e830d) If your app has multiple password requirements that can’t fit on a single line, add the requirements as bullet points directly below the form field. ![](https://cms.echobind.com/assets/591f6a20-d192-498d-a8b8-8be2d1d6a74c) ### Don’t make the user confirm their password, let them see their password Instead of making the user confirm their password, give them the ability to show or hide it using an icon or text toggle. Making users confirm their password decreases conversion and causes more user corrections. In [one case study](https://www.zuko.io/blog/should-you-use-confirm-password-on-your-forms-and-websites-case-study), the authors were able to increase the conversion rate by 56.3% by removing the confirm password field. In [Why the Confirm Password Field Must Die](https://uxmovement.com/forms/why-the-confirm-password-field-must-die/), UX Movement had this to say about the case study: > Once they removed the confirm password field and replaced it with an unmasking option, the number of user corrections decreased. Not only that, but it increased form starts, completions and the conversion rate. Not bad ROI for such a simple change. ![](https://cms.echobind.com/assets/690afb07-bd58-47a8-890b-ead5f9b33230) ### Link to the login screen For users who already have an account, make it easy to switch to the login screen by providing a link. ![](https://cms.echobind.com/assets/f13b8006-5fbe-4baf-923c-98a2dc86a39c) ### Validate once the user clicks submit instead of using live validation As Adam Silver points out in his article [The Problem with Live Validation and What to Do Instead](https://adamsilver.io/blog/the-problem-with-live-validation-and-what-to-do-instead/), live validation can cause many problems for users including users not noticing errors as they appear (which sort of defeats the whole purpose of validation). Instead of live validation Adam recommends validating the form only once the user has clicked `Submit`. This allows the user to clearly see which fields need to be fixed and fix everything at once rather than switching between answering questions and fixing mistakes. ## Login ### Allow the user to recover their password and put the link somewhere near the password field People forget passwords all the time. Your login form should make it easy for users to recover it. Put the `Forgot your password` link near the password field. I generally like to put it below the form field on the left for easy eye tracking. ![](https://cms.echobind.com/assets/bedaf889-37b5-4477-be3c-d43d2967c2f6) ### Allow for social login single sign-on (SSO) Single sign-on using social accounts is a modern convenience that many users have come to expect. It’s easier than typing out your email and having to remember multiple passwords for different apps. It can also reduce failed login attempts. To reduce cognitive load for users, we recommend adding no more than 3 social login options. According to the [State of Consumer Identity Trends 2023](https://www.loginradius.com/resource/consumer-digital-identity-trend-report-2023) report, Facebook and Google are the most popular social logins, so adding those is a good bet. You should only add a third option if you have evidence that the majority of your target audience uses another social platform more than Facebook or Google. ![](https://cms.echobind.com/assets/105bbb89-946d-4c0c-8554-29e7f28f0004) ### Allow for fingerprint authentication and FaceID when possible Similar to SSO, fingerprint authentication and FaceID give users a convenient alternative to the standard login method. The [State of Consumer Identity Trends 2023](https://www.loginradius.com/resource/consumer-digital-identity-trend-report-2023) report found that passwordless login (e.g. magic links, fingerprint authentication, FaceID, etc.) had the highest user retention of any authentication method. If you have the time and budget, adding alternative login methods, such as social SSO and passwordless login, gives users control over their experience and can increase retention. ### Remember what the user has already typed If a user enters incorrect login credentials, show an error message but do not remove the email and password they entered. The user should not have to re-type their email between password attempts, and the password they entered may just contain a typo that can be easily corrected. Similarly, if the user clicks `Forgot password`, the reset password screen should remember the email they entered on the login screen. ### Link to the signup screen For users who don’t already have an account, make it easy to switch to the signup screen by providing a link. ![](https://cms.echobind.com/assets/a5503346-e028-43aa-a74e-a7160e9b1546) ## Other Tips ### Don’t use “sign up” and “sign in” together Using terms for account creation and login that are too similar to each other, such as “sign up” and “sign in”, can be confusing for users. Instead, use terms that are distinct from each other. Options include: - Sign up / log in - Register / log in - Register / sign in - Create account / log in - Create account / sign in - Join / log in - Join / sign in As a side note, “sign up” is the verb “signup” is the noun. Same for “log in” / “login”. ### Don’t autofocus the first form field As Adam Silver points out in his article [The Problem with Automatically Focusing the First Input and What to Do Instead](https://adamsilver.io/blog/the-problem-with-automatically-focusing-the-first-input-and-what-to-do-instead/), autofocusing the first input leads to problems for keyboard use, screen readers, and on mobile. The simple solution is to put the user in control and let them focus the first input on their own. If you have other tips or best practices you follow when designing signup and login forms, let us know [on Twitter](https://twitter.com/echobind). --- [View on echobind.com](https://echobind.com/post/designing-signup-and-login-forms) --- # Embedding Remix in Directus > In this mad-science experiment, we combine the Directus CMS with the Remix framework in a single package that enjoys the features of both. _By Alex Anderson · 2023-10-16_ Who’s ready for some mad science? 🧑‍🔬 [Directus](https://directus.io/) is a self-hosted, all-in-one CMS platform written in JavaScript. You hook it up to some SQL database, build a data model, and it gives you a REST and GraphQL API for getting your data. Plus it manages files, users, roles and permissions, automated workflows, simple analytics dashboards, and all kinds of other stuff. PLUS, if you want you can create extensions to give it even more capabilities! [Remix](https://remix.run/) is a server-side framework that lets you create dynamic, server-side rendered apps with React and web platform primitives, like FormData, Request, and Response. It’s fast and powerful, but its most valuable feature is how easy it is to add great user experience to a site, like pending UI states, optimistic updates, and smooth error handling. It’s trivial to use Directus with Remix - just point a [Remix loader](https://remix.run/docs/en/main/route/loader) at your Directus endpoint to get whatever data you need for a route. But you’ll still need to find a place to host two separate apps - one for Remix and one for Directus. Wouldn’t it be nice if you could run both of them together? ## Peeking Inside Directus Directus itself is built on top of some pretty common JavaScript libraries, like [Knex](https://knexjs.org/) for building database queries and [Vue](https://vuejs.org/) for building UIs. Incidentally, it also uses [Express](https://expressjs.com/) as its web server. When you make what’s called an “endpoint extension”, Directus gives you an Express router which you can use to add custom endpoints to your Directus instance. ```javascript // An example endpoint extension. // You can access it at /greet/intro on your Directus instance export default { id: 'greet', handler: (router) => { router.get('/intro', (req, res) => res.send('Nice to meet you.')); }, }; ``` The Directus examples focus on using endpoint extensions to build local proxies to other services, like Stripe or Twilio. But endpoints don’t just have to be used for APIs. It’s a regular Express router, which means it can return whatever you want. As it turns out, Remix happens to have an Express adapter, which converts its `req` and `res` into Web API `Request` and `Response` objects for Remix to consume. Handy. …you see where I’m going with this. ## Mixing Oil and… Different Oil The plan is to have one repository that includes our Remix app, our Directus instance, and a Directus extension to connect them. To begin, we’ll create a basic Remix app with the `create-remix` CLI. ```javascript npx create-remix@latest ``` Doesn’t matter what template you use, they should all work. Next, let's make a new extension for Directus inside our Remix app folder. The purpose of this extension is to serve our static files and call the Remix request handler. We’ll use the handy extension SDK they provide: ```javascript npx create-directus-extension@latest ``` We’ll choose “endpoint” as our extension type and it’ll scaffold a simple extension for us. By default endpoint extensions make the route start with the name of the extension, but you can override that by passing in an “id” to the endpoint config, which becomes what it uses for the route. Now you might be wondering “Is Directus going to limit us to only have our app run at some sub-URL?” Funny enough, no. If you pass an empty string for the ID of your endpoint, your endpoint now lives at the top of the route tree and your Remix app can handle any request coming into the server (aside from those specifically handled by Directus, like `/admin` or `/assets`. > I imagine this is an oversight on the part of Directus - an unintended behavior. If they ever patch it so this doesn’t work (which I hope they don’t), you will have to host your Remix app at a specific sub-route. The [`publicPath`](https://remix.run/docs/en/main/file-conventions/remix-config#publicpath) Remix config will be helpful in this case. First things first, let’s serve our static assets. The `serve-static`is a handy way to do that. It will automatically find our files, apply caching headers, and pass on the request if a file isn’t found. Install it in your extension folder with NPM. ```text npm install serve-static ``` Now add handlers for the `public` and `public/build` folders. For file paths, we’re assuming that Directus is being run from the root of the Remix project, which is how we’ll set it up in a minute. ```javascript import { defineEndpoint } from "@directus/extensions-sdk"; import serveStatic from "serve-static"; import * as path from "node:path"; const __dirname = process.cwd(); const serve = serveStatic(path.resolve(__dirname, "public"), { maxAge: "1h", }); // Built files have hashed filenames, so they can be cached forever. const serveBuild = serveStatic(path.resolve(__dirname, "public/build"), { maxAge: "1y", immutable: true, }); export default defineEndpoint({ id: "", handler:(router, context) => { router.all("*", (req, res, next) => { // Handling for Directus URLs if (req.url.startsWith("/auth/login") || req.url.startsWith("/admin")) { return next(); } serveBuild(req, res, () => { serve(req, res, () => { next(); }); }); }); }, }); ``` > There might be better ways to do this than nesting `serve` inside `serveBuild`. Consider this implementation merely for illustrative purposes. Then we need to create our request handler. We’ll need to import our built Remix server and pass it to the Express request handler. This includes creating a [`getLoadContext`](https://remix.run/docs/en/main/route/loader#context) function, which grabs some data from the `req` object and the Directus context and makes them available to our Remix loaders and actions. ```diff + import { createRequestHandler } from "@remix-run/express"; + import * as url from "node:url"; import * as path from "node:path"; // ... + const BUILD_PATH = path.resolve("./build/index.js"); + const BUILD_URL = url.pathToFileURL(BUILD_PATH).href; + let build = await import(BUILD_URL); +function getLoadContext(context) { + return (req: any) => { + return { + ...context, + schema: req.schema, + accountability: req.accountability, + }; + }; +} export default defineEndpoint({ id: "", handler:(router, context) => { + const request handler = createRequestHandler({ + build, + getLoadContext: getLoadContext(context), + }); router.all("*", (req, res, next) => { // Handling for Directus URLs if (req.url.startsWith("/auth/login") || req.url.startsWith("/admin")) { return next(); } serveBuild(req, res, () => { serve(req, res, () => { + requestHandler(req, res, next); }); }); }); }, }); ``` Congratulations! That’s our entire extension. We could add [a bit more code to handle live reload](https://remix.run/docs/en/main/guides/manual-mode) and file watching in development, but this is all we need at the moment as a minimum viable product. We will need to get our un-built extension built and into the extensions folder… Except the extensions folder doesn’t exist yet! Directus will create one the first time we start it up. Let’s do that now. Run `npx directus@latest init` and follow the prompt. If you’re just playing around with this, choose SQLite so you don’t have to set up a separate database. Once that’s done, Directus will create an empty extensions folder and a `.env` file. Build your extension by running `npm run build` from within the extension folder. Then move the resulting `index.js` file to `/extensions/endpoints/{extension_name}/index.js` so Directus can pick it up. You can configure where that `index.js` file ends up by adding some config to your extension’s `package.json` file. This should do the trick to automatically put it in our extension folder: ```text // package.json { "directus:extension": { "type": "endpoint", "path": "../extensions/endpoints/{extension_name}/index.js", "source": "src/index.ts", "host": "^10.1.11" } } ``` We’ll also need to adjust our `.env` file a little bit by adding some things to the top. ```text # Where to redirect to when navigating to /. Accepts a relative path, absolute URL, or false to disable ["./admin"] ROOT_REDIRECT="false" # Required for the Remix app to work CONTENT_SECURITY_POLICY_DIRECTIVES__SCRIPT_SRC="array:'self','unsafe-inline','unsafe-eval'" ``` The first makes it so visiting `/` doesn’t automatically redirect to `/admin`. The second overrides some content security policy directives allowing the browser to execute some ` ``` When clicking on a record, the `on-record-click` handler parses the JSON object and then calls the `setCurrentSong` function (defined in `state.js`) to store the current `musicInfo` (song, album, artist, sound). ```javascript AFRAME.registerComponent("on-record-click", { init: function () { this.el.addEventListener("click", function (evt) { let json = this.components["on-record-click"].data.jsonData; if (json) { let musicInfo = JSON.parse(json); this.emit("setCurrentSong", musicInfo); } }); }, }); ``` Notice the `emit` function that passes the name of the setState handler (in this case “setCurrentSong”). This comes from the `aframe-state-component` package we imported earlier ### State.js Where state is stored and setState handlers are defined in the `state.js` file (shown below). Among those handler’s is the `setCurrentSong` function that was just shown above to be passed through the `emit` function when an album is clicked. Here you can see that the `musicInfo` is passed along as well and is set to current state. ```javascript AFRAME.registerState({ initialState: { buttonText: 'Play Music', songIsSelected: false, currentSong: '', currentAlbum: '', currentArtist: '', currentSound: '' isMusicPlaying: false, }, handlers: { **setCurrentSong: function(state, musicInfo) { state.songIsSelected = true; state.currentSong = musicInfo.song; state.currentAlbum = musicInfo.album; state.currentArtist = musicInfo.artist; state.currentSound = musicInfo.sound; },** setMusicPlaying: function(state) { let newButtonText; state.isMusicPlaying = !state.isMusicPlaying if(state.isMusicPlaying) { newButtonText = 'Pause Music' } else { newButtonText = 'Play Music' }; state.buttonText = newButtonText; } } }); ``` ### bind__ Thanks to `aframe-state-component` we can also access state and `bind__` its value to A-Frame entities to affect their render output. This is the UI Panel entity we created in index.html: ```html ``` Notice that the current song title entity contains a `bind__text` attribute which passes the `currentSong` stored in `state.js` to the `value` of the text attribute. Thus rendering the text of the current song! The same applies to the current artist and album entities. ## Resources - [Supermedium’s Collection of Aframe Components](https://supermedium.com/superframe/) is a nifty list of packages that appear in a lot of example projects on a-frame’s site. - Guide for building UI in A-Frame: [https://glitch.com/~aframe-building-ui](https://glitch.com/~aframe-building-ui) - The aframe-state-component: [https://www.npmjs.com/package/aframe-state-component](https://www.npmjs.com/package/aframe-state-component) --- [View on echobind.com](https://echobind.com/post/an-a-frame-state-management-example) --- # Server-Side Translations w/ NextJS > How I was able to get translations working on the server in Next.js. _By Matt Thompson · 2023-01-05_ You know the idea: if you have to spend a lot of figuring something out and/or it was hard to find on the interweb… you should probably write about it. If for no other reason than being kind to your future self. So, hello future me 👋🏼! Recently I spent way too long trying to get translations to work server-side inside a NextJS API route. I did not expect it to become the issue it did. At first, I blamed the weak coffee, but then I continued to dive in. To my surprise there wasn’t a well-documented answer. I validated some claims in Discord, got a handful of responses that led me down a decent path, and now here I am to share my findings. ## The Issue So, what was I trying to do… I’ve been working a ton with Twilio lately. I have started pulling together some concrete examples in an app for future us/me. Specifically, in this scenario, I was working with SMS and simply wanted to supply a translated body for the message going out. Easy enough, right? Grab the user's locale, translate the “Welcome, Johnny!” message, and fire away. In previous stacks translations become a class service util and, well, you just use them - anywhere and everywhere - without really thinking too much about it. Off I go to do just that. I set up my `public/locale` folder. Created an `sms` namespace with my test message, added to my API call, and… 💥 Huh?! Did I miss something in the config? I decided to validate on screen via the client. Yup, that worked?! What the… After searching around I came to find out that the majority of React/NextJS pieces are client driven utils. Meaning all that config and setup is meant for the React useHook. There wasn’t a non-client facing utility to be found… or at least the weak coffee was not helping me find it. Then I went hitting all the channels to see what I could be overlooking. ## The Solution Create a global i18n instance that can be passed around for both the Server and the client and leverage `i18next-fs-backend` for our API routes. Below you’ll see we have three methods the main `createI18nClient` and two utils I created that wrap the client for quick use `translator` and `translate`. The translator returns the client mapped to the namespace, locale, and translate going ahead and doing the good thing by returning our translated body text. ```typescript // https://discord.com/channels/752553802359505017/1046900384481951754/1047052679492419614 import path from 'path'; import { readdirSync, lstatSync } from 'fs'; import i18n, { InitOptions } from 'i18next'; import i18nextFSBackend from 'i18next-fs-backend'; // import { i18n as i18nConfig } from 'next-i18next.config'; import { I18n, InitPromise, CreateClientReturn } from 'next-i18next'; let globalInstance: I18n; const localesFolder = path.join(process.cwd(), '/public/locales'); type TranslatorProps = { template: string; ns: string[]; args: object; lng?: string; }; type UseTranslatorProps = { ns: string[]; lng?: string; }; type CreateInstanceProps = { locale?: string; namespaces: string[] | readonly string[]; }; const createI18nClient = ({ namespaces }: CreateInstanceProps): CreateClientReturn => { let instance: I18n; const config: InitOptions = { initImmediate: false, fallbackLng: ['en'], // NOTE: pass in locale to make this dynamic -- originally imported from 'next-i18next.config' compile issue in CI // fallbackLng: i18nConfig.defaultLocale, ns: namespaces, // lng: i18nConfig.locales.includes(locale) ? locale : i18nConfig.defaultLocale, lng: 'en', // preload for server side -- preload ['en'] preload: readdirSync(localesFolder).filter((fileName) => { const joinedPath = path.join(localesFolder, fileName); return lstatSync(joinedPath).isDirectory(); }), backend: { loadPath: path.join(localesFolder, '{{lng}}/{{ns}}.json'), }, load: 'all', // return empty string instead of key if something went wrong saveMissing: true, saveMissingTo: 'all', missingKeyNoValueFallbackToKey: true, parseMissingKeyHandler: (key) => { console.log('Missing Key:', key); return ''; }, // useSuspense to avoid first render issues react: { useSuspense: true }, // debugging info debug: false, }; if (!globalInstance) { globalInstance = i18n.createInstance(config); instance = globalInstance; } else { instance = globalInstance.cloneInstance(config); } let initPromise: InitPromise; if (!instance.isInitialized) { instance.use(i18nextFSBackend); initPromise = instance.init(config); } else { initPromise = Promise.resolve(i18n.t); } return { i18n: instance, initPromise }; }; export default createI18nClient; // returns 't' as you would expect export const translator = ({ ns, lng = 'en' }: UseTranslatorProps) => { const { i18n } = createI18nClient({ namespaces: ns, locale: lng }); return i18n.t; }; // Just do the good thing... I don't care about my options here. export const translate = ({ template, ns, args, lng = 'en' }: TranslatorProps) => { const { i18n } = createI18nClient({ namespaces: ns, locale: lng }); const { t } = i18n; return t(template, { ...args, ns }); }; ``` ## In Practice Because this import is now global util — they both work consistently the same, as expected! 🎉 ### Client Side ```typescript import { translator } from '../utils/i18n-translator'; ... const t = translator({ ns: ['common'] }); const welcomeMessage = t('welcome', { firstName }); ``` ### Server Side ```typescript import { translator } from '../utils/i18n-translator'; ... const t = translator({ ns: ['common'] }); const welcomeMessage = t('welcome', { firstName }); ``` ## Conclusion While there may be room for improvement here, I’m happy where this landed. I was able to start translating messages from my API as expected, and I now have a better understanding of NextJS and its translation setup. --- [View on echobind.com](https://echobind.com/post/server-side-translations-with-nextjs) --- # 3D Optimization for Web—How I Got a Model From 26MB Down to 560KB > For Q4 investment time, I worked on creating a scrollable, 3D website with a coworker using R3F. From this project, I’d like to highlight my process for optimizing 3D models for web. _By Jacob Galito · 2023-01-05_ With my [investment time](https://echobind.com/post/the-roi-of-self-directed-investment-time-for-software-engineers) at Echobind, as well as my own free time, I’ve been slowly learning webGL libraries such as three.js, Mozilla’s/Super Medium’s [A-Frame](https://aframe.io/), Google’s [model-viewer](https://modelviewer.dev/) and, more recently, [React-three-fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction)(R3F). For Q4 investment time, I worked on creating a scrollable, 3D website with a coworker using R3F. From this project, I’d like to highlight my process for optimizing 3D models for web. From that process, I was able to reduce the main 3D model from 26MB down to 560KB. ### **What is React-three-fiber?** Frameworks such as[ ](https://threejs.org/)[three.js](https://threejs.org/) make programming with webGL easier. Furthermore, libraries such as R3F—which abstracts three.js more—make working with 3D experiences on the web even easier. [Three.js example](https://codesandbox.io/s/three-js-example-0zgl3b) [R3F example](https://codesandbox.io/s/react-thee-fiber-example-qtd9gk) ### **Load time issues** One of the challenges that we faced while working on this scene was how to minimize load time on the client side. Our first approach was to create a load screen. However, since our 3D models files size was too large (26MB), it was taking too long to load. When it finally loaded, the interactive animations were slow and choppy. To correct these issues, I wanted to find a way to minimize the file size. ### **My approach to the problem** The primary contribution to the large file size was the complexity in our 3D model as well as its associated textures. So, I took on the responsibility of exploring how to minimize our loading time by optimizing our 3D model. I also discovered that there were too many materials and meshes in our scene. By minimizing both of these components, it would reduce the file size of the exported model. Here are the steps that I took: ## Step 1 - Optimizing meshes, deleting polygons and combining objects ![](https://cms.echobind.com/assets/01f7ee64-56a8-46da-96e8-073aeeda85a3) ### **Optimization** Since we were concerned about this scene not running well on the web, I wanted to remove any potentially unnecessary polygons on the meshes in the blender project. This was done using the decimation modifier. I typically start with reducing the mesh ratio to 0.5 and reduce lower if it doesn’t cause any unwanted artifacts. This step was repeated for most of the meshes. ### **Deleting Polygons** ![](https://cms.echobind.com/assets/f9175b04-f4fe-40be-8edc-d8a8276e077c) In an effort to help with unwrapping, as well as removing anything that won’t be showing up in the final view, I removed all polygons that were at the bottom of all meshes, such as the bottom portion of the snow mesh. This was done by going into edit mode > face selection using the select lasso. This step was repeated for most of the meshes. ### **Combining meshes** In order to get started with combining meshes, I needed to determine which meshes make sense to combine. In the end, I combined all objects in the scene that are on top of the snow on the ground and floor meshes—minus the picture frame. I decided to do this because they would all fit into the same UV space well and wanted them all to have the same size materials. The snow took up a considerable amount of space and adding it to the same UV space would reduce the scale of all of the objects; which would lower the quality of the textures for the objects as well. From there, I wanted to combine all meshes associated with the picture frame—minus the glass and photo—since I wanted to be able to add as much quality into the photo image. The glass needed its own material to prevent any transparency issues will other parts of the scene that didn’t need transparency. I renamed the meshes appropriately to their contents: `objects, frame, glass, photo, floor, snow` as well as their associated materials. ![](https://cms.echobind.com/assets/51d7b3f2-fe52-4417-8e8e-8e78da6875fa) This resulted in reducing the scene from 65 individual meshes to 6 objects of related meshes. Combining meshes actually increases the blender file size, but helped me to remove any potential duplicates in the scene and quickly select a large amount of polygons in the scene in order to manipulate them easier for the following steps. ![](https://cms.echobind.com/assets/c5e18905-c7dd-4f07-b5d3-8e9c1d1badf8) ## Step 2 - UV Packing Before I was able to unwrap and pack the UVs together, I needed to associate all of the polygons of each mesh into one material. At this point there were a lot of materials within the project. So, first what needed to be done was to remove all unused `orphaned` materials. By deleting all `orphaned` materials, I reduced the materials in the scene from 71 materials down to 6 materials. Here’s the results of the unwrapped `objects`: ![](https://cms.echobind.com/assets/faf98882-b6d0-4bcc-a921-b1d8531c7d5f) Here’s the results of the unwrapped picture `frame`: ![](https://cms.echobind.com/assets/cabf6d32-9423-406d-8190-2805222d7eee) All of the remaining meshes were unwrapped as well. ## Step 3 - Material IDs Material ID grants you the ability to quickly select a section of a mesh that you have determined will have a specific material added to that section. Instead of adding several materials to one mesh, which can add more to the files size of the model, we can take advantage of material IDs and translate these selections of a mesh into one single texture that can be used in step 5. To do this, I created various materials with high contrasting colors. After all of the polygons of the `objects` mesh and picture `frame` mesh had various colored materials added to it, I then baked the colors out so they were added to the UV coordinates of the meshes. ![](https://cms.echobind.com/assets/2880b6c7-4381-4aaf-8b67-4764de20f4f5) ![](https://cms.echobind.com/assets/26aa6e9d-6772-4d27-b665-180714b7c0fd) ![](https://cms.echobind.com/assets/a2fa07cf-59d0-4c22-8839-fd72f050fdab) ## Step 4 - Baking Material IDs The process of baking, for this purpose, is to take all of the color information, from the various colored materials I had just added, and project those colors onto a texture. This is the resulting material ID maps that were baked out, as shown above. Some things to note before baking materials in Blender: - Change the render mode to `cycles` - Turn off any `direct` and `indirect` lights - Ensure you have selected the diffuse map in shaders viewport - Select the duplicate mesh that has the colors added - Have the mesh with the unwrapped UVs be your active selection - Ensure that `Bake Type` is `Diffuse` - Press `Bake` ![](https://cms.echobind.com/assets/ad5dba1f-c181-4567-9b45-82c7246a437a) ![](https://cms.echobind.com/assets/036c456a-a307-4592-97f5-6d16cc673244) ![](https://cms.echobind.com/assets/fd329e66-6c86-4a37-bda9-b4baf4245ece) ![](https://cms.echobind.com/assets/823d0641-b929-4b8c-8df7-b8f720ddae0a) The final step in Blender was exporting everything as .fbx as well as export out all material ID textures. ## Step 5 - Material Creation in Substance Painter ![](https://cms.echobind.com/assets/83a9692b-4b8e-4b06-9cac-37f2caa121a9) Substance Painter is one of my favorite 3D tools. It can make the texturing process so much fun and contextual. It’s basically photoshop for 3D models. ### **Set up** ![](https://cms.echobind.com/assets/67f39b59-726f-4c2b-ae9a-8f53a498bf80) While importing the .fbx, I also imported in the material ID textures for `objects` mesh and picture `frame` mesh. ### Masking and selecting material IDs ![](https://cms.echobind.com/assets/2daf818b-cc37-4db0-8407-5af0895c0225) After baking the mesh maps (normals, world space normals, ambient occlusion, curves, etc.) I then created folders for each color of the `objects`mesh and picture `frame` mesh and used a mask to select each material id to attribute to the mask. This is how we’re able to add different colors and textures to part of a mesh. ![](https://cms.echobind.com/assets/c81a1e44-af56-4a4c-bdef-8ec2ebe80560) ### Exporting and **Combining baseColor and ambient occlusion** ![](https://cms.echobind.com/assets/ec784921-8ff6-4edb-85f6-46afca063d62) Since the desired result we’re going for is not so realistic, I decided to only export out the `baseColor` and `ambient occlusion` but since I wanted to add the shadow/lighting from ambient occlusion to the baseColor I needed to combine the two into one texture. In order to do this, I needed to add a new preset in the `export textures` viewport. ## Step 6 - Adding baseColor in Blender ![](https://cms.echobind.com/assets/97bb36fb-f923-4b36-a529-9b264048b862) This step is really quick. I just needed to add the baked texture results from Substance Painter and add them to each material’s `baseColor`. After that, I exported everything as a `.glb`. At this point, the exported file size was **5.1MB**. ![](https://cms.echobind.com/assets/80edac58-9a59-4351-81b6-b11e73db4557) ## Step 7 - glTF Compression ### [gltf.report](http://gltf.report) tool Now that the model is textured and exported from Blender, I uploaded the model to [gltf.report](http://gltf.report) to compress the `.glb` more. After uploading the model in to gltf.report, I then clicked on the `script` tab. From there I reduced the texture size from 1024 x 1024 down to 128 x 128 and clicked `Run` then `Export` afterward. ![](https://cms.echobind.com/assets/12826323-405d-414e-ab3f-8265cb738fbe) The results got the file from **5.1MB** down to **3MB**. The resulting reduced mesh was then added to the project under `/public/meshes/`. ### **gltfjsx command-line tool** Before running the next step, it’s important to install `nvm` and install an earlier version of node. Otherwise, you’ll probably get error when running `gltfjsx`. ```c NVM ls NVM use 14.20.0 ``` Before I could run the command-line tool, I needed to change directories from the project folder to where the mesh is located. Otherwise the results would be added to the project folder. ```c cd /public/meshes/ ``` Now, I was able to run `gltfjsx` ```c npx gltfjsx main_scene.glb -T ``` The result of running `gltfjsx` provides a smaller `.glb` file as well as a `react.js` component of the 3D model. The results reduced the file from **3MB** down to **581KB.** If you don’t want to mess with installing gltfjsx, and earlier node versions, you could also go to [https://gltf.pmnd.rs/](https://gltf.pmnd.rs/) but results may vary. And here’s the final result at **560KB** ![](https://cms.echobind.com/assets/b885b64b-a264-4f16-aeae-564a5abd7b88) Here are the results from `gltfjsx` for `main_scene.js`: ```javascript /* Auto-generated by: https://github.com/pmndrs/gltfjsx */ import * as THREE from 'three' import React, { useRef } from 'react' import { CycleRaycast, useGLTF } from '@react-three/drei' import { GLTF } from 'three-stdlib' export function Main(props) { const { nodes, materials } = useGLTF('/meshes/main_scene.glb') return ( {console.log('frame clicked')}} geometry={nodes.frame.geometry} material={materials.frame} position={[-0.02, 1.27, -1.89]} rotation={[Math.PI, -0.02, Math.PI]} /> props.setModalIsOpen(true)} geometry={nodes.photo.geometry} material={materials.photo} position={[-0.02, 1.24, -1.89]} rotation={[Math.PI, -0.02, Math.PI]} scale={0.09} > {/* testing out how to add cursor pointer based on mouse over the photo */} {/* docs here https://github.com/pmndrs/drei#cycleraycast */} {/* example use of useCursor https://codesandbox.io/s/ny3p4?file=/src/App.js:977-983 */} console.log(objects)} // Optional onChanged event onPointerOver={() => {console.log('hovered')}} /> ) } useGLTF.preload('/meshes/main_scene.glb') ``` [Here’s the full 3D website project](https://holidaycard.vercel.app/) ## Thanks We wouldn’t have been able to learn this process—among everything R3F and three.js—if it wasn’t for Bruno Simon’s new R3F lessons, and Paul Henschel’s Gumroad course. Thank you both for sharing your knowledge! ### Resources [three.js - JavaScript 3D library ](https://threejs.org/) [A-Frame - Make WebVR](https://aframe.io/) [3D Model-Viewer Embed ](https://modelviewer.dev/) [React Three Fiber Documentation](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) [Blender how to Reduce Poly Count and Bake Textures](https://youtu.be/Yx9TvvnxCAM) [What are colour IDs and how do we use them?](https://youtu.be/4tbE5dp4-Mo) [I wish I knew this before using React Three Fiber](https://youtu.be/DPl34H2ISsk) [gITF Report ](https://gltf.report/) [GitHub - pmndrs/gltfjsx: Turns GLTFs into JSX components ](https://github.com/pmndrs/gltfjsx) [GLTF → React Three Fiber ](https://gltf.pmnd.rs/) [Bruno Simon’s course](http://threejs-journey.com/) [Bruno’s Twitter](https://twitter.com/bruno_simon?s=20&t=8hNOPK57upYJuhYW3EhYow) [Paul Henschel’s course](https://0xca0a.gumroad.com/l/B4N4N4S) [Paul Henschel’s twitter](https://twitter.com/0xca0a?s=20&t=8hNOPK57upYJuhYW3EhYow) --- [View on echobind.com](https://echobind.com/post/3D-Optimization-for-Web-26mb-down-to-560kb) --- # How to Use Notion Databases > Notion’s databases offer more substantial spreadsheet functions to structure and track large datasets . _By Claire Surma · 2023-01-05_ Our fully-remote Echobind team relies on Notion for internal process documentation. We are fans of the tool because it allows for easy collaboration and manipulation of data. As a Q4 initiative, we updated internal checklists so we can start 2023 off with streamlined and consistent processes. To do this, we leveraged Notion’s databases because they offer more substantial spreadsheet functions to structure and track large datasets. ## This article walks you through: - How to Create a Table Database - An Overview of the Five Database Views - How to Filter Your Database Properties --- ## How To Create a Table Database in Notion 1. Click `New Page` found at the bottom left of your Notion screen 2. Select `Table` from the database options in the New Page menu. ![](https://cms.echobind.com/assets/d972947a-8eb7-41c1-bb30-717de4886bfc) 3. Enter pieces of content you want to track into the first column. *Unlike spreadsheets, each content cell in the first column can open up into its own Notion page where you can view all properties and add additional notes. ![](https://cms.echobind.com/assets/fb50aaab-a4a4-41b7-989d-21f42c0961ea) 4. For the remaining columns, you can add properties you want to include in your database by clicking the `+` on the first row. You can edit property names and type by clicking on the current title. My favorites include: **Select Tags**: To easily sort the status of the to-do. ![](https://cms.echobind.com/assets/22818fb6-6cd9-4ca0-b630-3944639d3948) **Due Dates:** To visualize and track when tasks must be complete. You can even use notion to set reminders. ![](https://cms.echobind.com/assets/c0907987-9c53-44b7-93d2-16049eb46355) **Person Responsible:** To ensure everyone is aware of who is taking lead on tasks. ![](https://cms.echobind.com/assets/f9244218-5a77-4d49-9950-83f19d95cf04) --- ## Overview of Notion Database Views Once you enter the information you want to track in your database, you can add additional database views by clicking the `+` to the right of your current table view. You can add multiple views to the same database. This is helpful to view the same information in different ways and allows you to quickly navigate between views to fit your needs. ![](https://cms.echobind.com/assets/4e285150-7491-4bd1-8278-443b3a99c184) Each of the view options will restructure the information you inputted in the original table view. These views include: 1. **Board:** This view will structure your data into a kanban board for easy project planning and flow tracking. ![](https://cms.echobind.com/assets/a56dcc68-652e-4f7f-8cf6-306aaee844a2) 1. **Timeline**: This view will show your data in a gantt chart for project scheduling and data planning. ![](https://cms.echobind.com/assets/86324b60-0792-440c-8678-513f35344cc7) 1. **Calendar:** This view inputs your data into a calendar month for event planning and scheduling. ![](https://cms.echobind.com/assets/4ab764a1-8072-435e-83d1-8b44c19eddd9) 1. **List:** This view is a simplified list of all pieces of content you track in the first table view column for bookmarks and notes. ![](https://cms.echobind.com/assets/123ea2c2-3d72-4bf5-b670-84b3bf327dc5) 1. **Gallery:** This view shows a grid of cards that can be helpful for mood boards, index cards, and recipes. Each card can be edited to show images contained in the `Files & media` property or content inside each page. ![](https://cms.echobind.com/assets/51e531ab-bc16-4f51-90e8-db93b8b721fd) --- ## How to Filter Your Database Properties Within any of the database views, you can add filters to easily sort and categorize your data set based on inputted properties. These filters can be simple (based on one property) or advanced (dependent on multiple property criteria). ![](https://cms.echobind.com/assets/151e401c-8a06-49f2-8ec9-a1b247021202) To add a filter in your database: 1. Click `filter` at the top right 2. From the dropdown, select the property to filter by. 3. This filter will now be listed at the top of your database. You can then click the property filter to choose how you’d like to filter your view. ![](https://cms.echobind.com/assets/f29e6210-784f-4826-8106-2dea094b2406) 1. When one filter or more filter has been applied, you can add another by clicking `+ Add Filter` at the top of your table. There is no limit to the number of filters you can apply! --- [View on echobind.com](https://echobind.com/post/how-to-use-notion-databases) --- # Test Stripe Subscriptions with Test Clocks > Testing Stripe Subscription is tricky. Test clocks are like micro-universes that can simulate any advancement in time. _By Alex Anderson · 2023-01-05_ **tl;dr**: Testing Stripe Subscription is tricky. Test clocks are like micro-universes that can simulate any advancement in time. Any customers created in the test clock universe, and any subscriptions attached to those customers, participate in the time travel. [Stripe Subscriptions](https://stripe.com/billing) provide an easy way to have customers pay on a regular basis. There’s a lot of opportunity for complicating them, though. Subscriptions support trial periods, when the customer isn’t charged for a number of days, or [Subscription Schedules](https://stripe.com/docs/billing/subscriptions/subscription-schedules) which let you change the products or discounts on a subscription. Testing these features is tough since we can’t control the flow of time, which has a pesky habit of passing at one second per second. Theoretical time travel methods, like [yeeting yourself at the speed of light](https://en.wikipedia.org/wiki/Twin_paradox), likely won’t help in this situation. Fortunately, Stripe Test Clocks give us a way to control the flow of time for specific subscription objects. ### [Test Clocks](https://stripe.com/docs/billing/testing/test-clocks) There are a few steps to use test clocks: 1. Go to the Stripe dashboard and turn on test mode. 2. [Create a test clock](https://dashboard.stripe.com/test/test-clocks). 3. Create a customer in that test clock. 4. Create a subscription of that customer. 5. Advance the test clock into the future by a number of days or months. 6. Check the results. All of this can be done from the Stripe API as well. Just pass a `test_clock` parameter when creating your Stripe customer, and any subscriptions attached to that customer will advance with the test clock. I recently needed to test a complicated subscription behavior. The app needed the ability to skip a customer’s subscription payment for one month. It did this with subscription schedules — it would apply a one-month free trial to the subscription while pushing the end date back by a month. So a customer with a 6 month subscription that skipped two months of payments would still only pay 6 times, with the payments split across eight months. ![A list of invoices showing the initial payment and then six months of subscription payments with two skips](https://cms.echobind.com/assets/703a7e5b-7a29-4075-9718-3c97d0ab421f) I tested this by first creating a test clock and a customer in that test clock. I took the customer ID and manually inserted it into my apps test database for a new test user. ![](https://cms.echobind.com/assets/2bbfdc0a-01b0-474a-915e-fc43418a4840) Now anything that I do in the app as that customer, including creating new subscriptions, will be included in the Stripe Test clock. I created a 6 month subscription, which I could see in the Test Clock dashboard. ![](https://cms.echobind.com/assets/226d4266-168a-4c40-a771-4d11eedb151b) Next, I used the “Skip Payment” feature in my app, which updated the subscription schedule like I described above. I checked the subscription to see the month long trial added and the end date pushed back by a month. Then to make sure everything sorted out correctly, I advanced my test clock by a few months and checked the subscription again. When I initially built this feature, I found a bug that added a prorated amount to the subscription because of the mid-subscription trial period. If I hadn’t used test clocks to test this feature, the bug would have persisted to production causing customers to overpay. After advancing the clock a few times, I checked the end state of the subscription. Once I found all of the payments and skipped months exactly the way I expected, I knew the feature was ready to ship. --- [View on echobind.com](https://echobind.com/post/test-stripe-subscriptions-with-test-clocks) --- # Extending Types for Prisma Extensions in NextJS > The issue is that now the type _By Matt Thompson · 2023-01-04_ If you read my [previous post](https://echobind.com/post/playwright-with-next-auth), you’ll note that I was very excited to try out Prisma’s new [Extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions) alongside their [Middleware](https://www.prisma.io/docs/concepts/components/prisma-client/middleware) and try out various use cases. When test driving I did hit a snag rebuilding the Types for our new extension to pull through. If you are running Prisma with NextJS you’ve probably followed this initial best practice doc: [https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices](https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices) Here we define `prisma` in the `global` object so that we don’t create a bazillion instances of Prisma and have the world crashing down on us. If you are using Typescript, this `global` instance was probably Typed. in our case we had: ```typescript declare global { // eslint-disable-next-line no-var var prisma: PrismaClient | undefined; } ``` ## The Issue The issue is that the type `PrismaClient` has no clue about our new Extensions or the types within. In this use case, we are attempting to create a new `signUp` method on the `User` model. ```typescript const extendedPrisma = prisma.$extends({ model: { user: { async signUp(args: SignUpArgs): Promise { return await prisma.user.create({ ...args, }); }, }, }, }); ``` So how do we fix this?… ## The Solution Turn `const prisma` from the previous example into a function that returns the newly created `extendedPrisma` context. When setting the global prisma value we use this value and Type instead. Now when we import `prisma` it should include our new model action `signUp` and type validations as seen below. ### The Code ```typescript import { Prisma, PrismaClient, Profile, User, } from '@prisma/client'; export const defaultUserSessionIncludes = { profile: true, } as Prisma.UserCreateArgs['include']; export type UserWithRelations = User & { profile: Profile; }; // Set default prisma logs. More logs in debug mode. const logOptions: Prisma.LogLevel[] = process.env.DEBUG ? ['query', 'error'] : ['error']; type UserCreateArgsWithProfile = Omit & { // Require Profile for signup profile: Prisma.UserCreateArgs['data']['profile']; }; // mimics Prisma.UserCreateArgs type SignUpArgs = { data: UserCreateArgsWithProfile; include?: Prisma.UserCreateArgs['include']; }; const extendedPrismaClient = () => { const prisma = new PrismaClient({ log: logOptions, }); const extendedPrisma = prisma.$extends({ model: { user: { async signUp(args: SignUpArgs): Promise { return await prisma.user.create({ ...args, }); }, }, }, }); return extendedPrisma; }; export type ExtendedPrismaClient = ReturnType; /** * Instantiate prisma client for Next.js: * https://www.prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices#solution */ declare global { // eslint-disable-next-line no-var var prisma: ExtendedPrismaClient | undefined; } export const prisma = global.prisma || extendedPrismaClient(); if (process.env.NODE_ENV !== 'production') { global.prisma = prisma; } export async function disconnect() { await prisma.$disconnect(); return true; } export async function connect() { await prisma.$connect(); return true; } ``` ![](https://cms.echobind.com/assets/03b90276-0ad6-4326-91aa-5a23880b80cd) ### If you are using Next-Auth One other note if you are using Next-Auth. We kept hitting a snag with the types for Next-Auth’s `PrismaAdapter` Again, it expects a type of `PrismaClient` but we want to pass our new `ExtendedPrismaClient` type instead. There was a weird type of error trying to recast this for some reason complaining about Prisma’s `$use` missing on our type. Remembering this is still in the **Experimental Phase.** We didn’t try to dive in too hard here, rather we cast as `unknown` first before passing along the types expected for the adapter. While not ideal, it does work and is something we’ll monitor as Prisma continues to wrap this feature up. ```typescript export const authOptions: NextAuthOptions = { adapter: CustomPrismaAdapter(prisma as unknown as ExtendedPrismaClient), ... } function CustomPrismaAdapter(p: ExtendedPrismaClient) { const prismaAdapter = PrismaAdapter(p as unknown as PrismaClient); ... } ``` ## That’s all folks! Remember to restart that Typescript Server folks. 🙂 I kept battling this a few times only to realize I needed to “turn it off and on again”. Once set, you’ll be ready to keep tacking on new extensions and middleware! --- [View on echobind.com](https://echobind.com/post/extending-types-for-prisma-extensions-in-nextjs) --- # Creating a Brand Guide From Scratch: Documenting Echobind’s New Visual Identity > Echobind launched a new website and visual identity near the end of 2022, and with that came the need for a brand guide to help all us use our brand assets in a way that consistently communicates who we are. _By Kaila VanSumer · 2023-01-04_ The past few months, Echobind's design team has been hard at work creating a new brand guide for the company. We launched a new website and visual identity near the end of 2022, and with that came the need for a brand guide to help all employees use our brand assets in a way that consistently communicates who we are to potential clients and new hires. ## **What is a Brand Guide?** A brand guide is essentially a manual for a company's brand. It includes all of the important information about the brand's visual identity, such as the logo, colors, typography, and voice and tone. It's important for a company to have a brand guide because it helps ensure that their brand is being presented consistently across all channels and platforms. Brand consistency drives brand recognition and helps build trust with clients. When a brand is consistently presented, it becomes easier for customers to recognize and remember, which can lead to increased loyalty. ## **How We Created Our Brand Guide** ### Auditing Our Existing Brand We started with a brand audit. We used [Figjam](https://www.figma.com/figjam/) to collect our previous brand guides, our website redesign files, and other brand decks for inspiration. Here’s what the Figjam file looked like: ![](https://cms.echobind.com/assets/cab1bbe4-e743-4741-8aa3-dc503fc9ad9d) ### Defining the Outline Once we completed our audit, we outlined the sections we wanted to include in our new brand guide. Many brand guides include more detailed sections on things like target market, buyer personas, illustration, iconography, and other topics, but we narrowed our list down to only the essential parts of our brand that were directly relevant to the brand collateral our employees create on a regular basis. Here’s the table of contents we decided on: ![](https://cms.echobind.com/assets/5feba655-750c-4477-8c07-67c6c49b53a7) ### Writing the Content Next, we wrote the content for each section and designed slides using [Figma](https://www.figma.com/) (our favorite design and prototyping tool), dropping in helpful visuals and assets along the way. We organized the slides in Figma from left to right, giving a title slide to each section. Here’s what the work in progress looked like: ![](https://cms.echobind.com/assets/4a4d584a-26b3-44ed-a0bb-a0d4d2cc0c8f) In addition to guidelines about our brand's visual identity, we also included some general tips on how to improve your design skills. We knew many of the employees who would be using this guide would not be full-time designers. Adding general tips empowers our non-designer colleagues to create with confidence without feeling like they need to ping our design team before sending out a newsletter or posting to social media. Here’s the short list of design tips we included: ![](https://cms.echobind.com/assets/5a1ad18f-79b6-4ac7-ba5d-8915b9780c98) Where appropriate we linked to useful assets like our repository of approved logos and our component library in Figma. We also wrote content on how to use the guide to make it easy to navigate based on what you’re trying to accomplish - sort of a Choose-Your-Own-Adventure Brand Guide. While we still need to write a section for imagery and accessibility, we’ve made a lot of progress on putting together a guide that helps define the most important parts of our brand and makes it easy for all Echobind employees to use our brand assets correctly. ### Our 2022 Brand Guide ### Free Brand Guide Checklist If you are creating a brand guide of your own we’ve created a free Brand Guide Checklist that you can get below. If you use it, be sure to tweet us [@echobind](https://twitter.com/echobind) with a picture of your own brand guide. [Download Brand Guide Checklist](https://www.dropbox.com/s/dfktgrev6nxbipv/Brand%20Guide%20Checklist.pdf?dl=0) --- [View on echobind.com](https://echobind.com/post/creating-a-brand-guide-from-scratch) --- # Echobind’s Virtual Holiday Event > There are many fun options for virtual holiday celebrations, and our team landed on a gingerbread house competition this year. _By Krystalyn Bauer · 2022-12-22_ The holidays are a wonderful time for celebrating and gathering, and being a fully remote team should be no exception. There are many fun options for virtual holiday celebrations, and our team landed on a gingerbread house competition this year. As a team of engineers, designers, and strategists, this was right up our alley. To spice it up, we found a few different styles of gingerbread house kits from one retailer: a Barn, Camper and Candyland house. The team chose their own style, we coordinated the purchase, and the competition allowed for any additions. Some of the gingerbread houses collapsed under pressure, some of them were impressively built just like the display, and some of them were imagined into incredibly unique and creative structures. We voted on three winners who received prizes, but the magic of this event was really in the anticipation of the competition and sharing our displays. Being able to step away from our screens and use our creativity and skills in a fun way was a gift in itself. We would definitely recommend this holiday event 10/10 and will be doing again. Even if a gingerbread house competition is not quite your team’s style, finding time to gather your team to celebrate the holidays is a great way to boost morale, lift spirits and end the year on a high note. Check out some of our awesome gingerbread builds, and have yourself a happy little holiday! ![](https://cms.echobind.com/assets/d41ba104-b55f-4a55-9aba-89e6d101ffa5) --- [View on echobind.com](https://echobind.com/post/virtual-holiday-event) --- # Instant Site Updates with On-Demand Revalidation and Directus > Live the CMS dream by having your changes instantly appear whenever you update your CMS while not sacrificing page load speeds. _By Alex Anderson · 2022-12-15_ Everybody loves a fast website, but there’s a balance between page speed, capabilities, and developer experience. For a lot of websites, a Static Site Generator like Next.js ticks all the right boxes. They essentially pre-cache every page in the site at build-type. But that’s also the biggest downside to SSGs — long build times. When your website pulls in data from many remote sources, it’s tough to know what pages changed and which stayed the same. So they just build all of them. When your site is based on lots of rapidly-updating data with a lot of pages, that’s a recipe for agonizingly long build times. Next.js has a bunch of features to help with this. `getStaticPaths` lets you choose which pages to pre-build and which to generate at runtime when a user requests them. And [Incremental Static Regeneration](https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration#on-demand-revalidation) will show visitors the pre-built pages while generating a fresh version in the background on a revalidate interval. ```javascript export const getStaticProps = async (context) => { const data = await fetchBlogData(context.params.slug); if (!data) { return { notFound: true }; } // Revalidate the page at most once every 60 seconds return { props: data, revalidate: 60 }; }; ``` It’s a great middle-ground solution - users still get fast page loads and they’ll eventually get the freshest data. But with a pull-based architecture like this, there’s not a lot more we can do to make this load faster. Instead, we need some kind of notification from our data providers that certain pages need to be updated. ### [On-Demand Revalidation](https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration#on-demand-revalidation) For the longest times, static hosts have offered webhooks that let CMSs and other data providers say when a site should be rebuilt with new data. For example, a new blog post is added, the webhook is called, the site is rebuilt, and after a time the blog post is live. On-Demand Revalidation is a feature of Next.js that takes this idea and lets you target specific pages with surgical precision. Instead of a webhook for the entire site, On-Demand Revalidation starts in a Next.js API route. In it, we can call a special API on `res.revalidate` which lets us pass the page path to revalidate. When we call it, Next.js will revalidate just that page in mere moments, publishing the freshest data possible in mere moments. ```javascript export default async function handler(req, res) { // Check for secret to confirm this is a valid request if (req.query.secret !== process.env.REVALIDATION_SECRET) { return res.status(401).json({ message: "Invalid token" }); } try { await Promise.all([ res.revalidate(`/post/${req.query.post}`), res.revalidate("/blog") ]); return res.json({ revalidated: true }); } catch (err) { // If there was an error, Next.js will continue // to show the last successfully generated page return res.status(500).send("Error revalidating"); } } ``` Notice that we’re calling `res.revalidate` for both the individual post page and for the blog index page. This makes it so any new blog post or updated titles correctly appear in the blog index. If we had separate index pages for each blog post tag, we’d want to revalidate those too. Now we just need a way to call this whenever we change a post. ### Calling Webhooks with Directus Flows Every CMS and data provider is different. Hopefully yours has some way to perform some logic, like calling a webhook, when a change happens to some part of data. In [Directus](https://directus.io/), we do this with flows. ![](https://cms.echobind.com/assets/edeaf94f-c8e7-4398-9bfa-c378cc7f8d9c) Flows allow us to respond to _triggers_ by chaining _operations_ to each other. We’ll configure an Action trigger which listens for the `item.create`, `item.update` and `item.delete` scopes on the `Posts` collection. ![](https://cms.echobind.com/assets/5771a402-d66c-47cd-9bcf-6bbcc4359343) > Debugging flows can be a little bit tricky. Fortunately, the flow’s sidebar shows a running log of all of the times the flow runs. This can help track the changes in the data as the flow progresses through the operations. Next, we need to fetch the post object from the database so we know what slug to send to our revalidate API. This is included in the `item.create` actions, but not always for `item.update`, so we need to grab it ourselves. For that, we’ll use a `Read Data` operation that fetches from the Posts collection using the `$trigger.keys` as the ID to fetch. ![](https://cms.echobind.com/assets/aceee599-97d0-4ed5-b131-a192d457f093) > `$trigger.keys` comes from the trigger. It’s a list of IDs for all of the items that were affected. Run your flow and check the logs to see the other data that is provided by the trigger. The final operation is to send the post slug to the API route webhook. In the code above you can see the post slug is on the `post` query param. We also pass a `secret` query param that is included in our Directus flow to keep third-parties from abusing our revalidate endpoint. ![](https://cms.echobind.com/assets/ca1294ed-d67a-4a2c-8bc2-6d4c10f36fe1) That’s all we need! After saving the flow, we can publish, edit, or delete blog posts and the published site should update almost instantly, or at very least significantly faster than the 60 second revalidate timer. Of course, this could be expanded to many other uses. An e-commerce website could list remaining quantity on an item that is always up-to-date. A comments section could be statically rendered while still showing the latest comments as soon as they are posted. That’s what the power and flexibility of this API can do for you. --- [View on echobind.com](https://echobind.com/post/instant-site-updates-with-on-demand-revalidation-and-directus) --- # Playwright with Next-Auth and PrismaOverview > Let's go over how to quickly configure Playwright with Next-Auth in a Next.js application. _By Matt Thompson · 2022-12-05_ You may have heard some rumblings in previous [blogs](https://echobind.com/post/why-we-ditched-graphql-for-trpc) and [tweets](https://twitter.com/alexdotjs/status/1589960174495625216) about Echobind moving to tRPC, and our team working on what we are calling [Bison 2.0](https://github.com/echobind/bisonapp). While tRPC is exciting, it is not the only change we are looking into. Over the past year, we have consistently started to leverage [Next-Auth](https://next-auth.js.org/), and have hit countless time sinks with Cypress. Next-Auth obviously plays into our stack built around NextJS, but testing it with Cypress always proved to be a bit… wonky. Having to build custom commands to use node and DB Factories, intercepting, mocking, and worst of all those `.then` waterfalls — Gross. Insert [Playwright. ](https://playwright.dev/) I’m happy to say Playwright has A LOT going for it out of the box in comparison. Today, I wanted to go over how to quickly configure Playwright with Next-Auth. I was excited to see Playwright had already taken Authentication into account out of the box. There are many options, but the most exciting for me is the ability to save our user sessions into a `storageState` for multiple user roles and re-use them in our specs. No more `beforeEach` where we need to check the Database, create a User, Login, intercept the call, mock the return, wait for the redirect, oh wait what was I testing again… Oh right, As an Admin I simply want to log in. Geez Playwright has a whole section on Authentication, but we are going to specifically look at the multiple signed roles section found [here](https://playwright.dev/docs/test-auth#multiple-signed-in-roles). ## Sounds great! Where’s the code… If you haven’t set up Playwright before, follow their quick setup here. [https://playwright.dev/docs/intro#installing-playwright](https://playwright.dev/docs/intro#installing-playwright) Once set, you should have a `playwright.config.ts` file. (You did choose Typescript right?!) We are going to add two lines to our default config. A `globalSetup` and `globalTeardown`. ```typescript // playwright.config.ts const config: PlaywrightTestConfig = { globalSetup: require.resolve('./tests/e2e/global-setup'), globalTeardown: require.resolve('./tests/e2e/global-teardown'), } ``` If you are familiar with Jest, or any other testing lib, this should sound straightforward enough. We are going to have two files that do something before and after ALL of our tests run. Here I’ve included these in my `test/e2e/` directory, just be sure you have the right file path and we’re all set here. Let’s take a look at our `global-setup` file. (I’ve also included my `constants` file for reference) ```typescript // tests/e2e/global-setup.ts import { chromium, FullConfig } from '@playwright/test'; import { Role } from '@prisma/client'; import { UserFactory } from '../factories'; // Shared Below... import { ADMIN, APP_URL, LOGIN_URL, USER } from './constants'; async function globalSetup(_config: FullConfig) { // These User Factories are specific to our boilerplate // The goal here is to create an Admin and User for your future specs const _adminUser = UserFactory.create({ email: ADMIN.email, emailVerified: new Date().toISOString(), roles: [Role.ADMIN], password: ADMIN.password, profile: { create: { firstName: ADMIN.firstName, lastName: ADMIN.lastName, }, }, }); const _user = UserFactory.create({ email: USER.email, emailVerified: new Date().toISOString(), roles: [Role.USER], password: USER.password, accounts: {}, profile: { create: { firstName: USER.firstName, lastName: USER.lastName, }, }, }); // The fun part! const browser = await chromium.launch(); const adminPage = await browser.newPage(); await adminPage.goto(LOGIN_URL); await adminPage.locator('data-test=login-email').fill(ADMIN.email); await adminPage.locator('data-test=login-password').fill(ADMIN.password); await adminPage.locator('data-test=login-submit').click(); await adminPage.waitForNavigation(); await adminPage.waitForURL((url) => url.origin === APP_URL, { waitUntil: 'networkidle' }); // This saves everything about `adminPage` so far into a named `storageState` await adminPage.context().storageState({ path: ADMIN.storageState }); const userPage = await browser.newPage(); await userPage.goto(LOGIN_URL); await userPage.locator('data-test=login-email').fill(USER.email); await userPage.locator('data-test=login-password').fill(USER.password); await userPage.locator('data-test=login-submit').click(); await userPage.waitForNavigation(); await userPage.waitForURL((url) => url.origin === APP_URL, { waitUntil: 'networkidle' }); // This saves everything about `userPage` so far into a named `storageState` await userPage.context().storageState({ path: USER.storageState }); // We are done for now :) await browser.close(); } export default globalSetup; ``` ```typescript // tests/e2e/constants.ts export const APP_URL = 'http://localhost:3001'; export const LOGIN_URL = 'http://localhost:3001/login'; // This is primarily args for our DB Factories // storageState is the named path/file where Playwright will save our session export const ADMIN = { email: 'brainiac@lex.com', firstName: 'Lex', lastName: 'Luther', password: '1amBrainiac!', storageState: './temp/adminStorageState.json', }; export const USER = { email: 'clark@thedaily.com', firstName: 'Clark', lastName: 'Kent', password: 'krypton8!', storageState: './temp/userStorageState.json', }; ``` Let’s walk through this in a bit more detail. After creating our Users for future scenarios, we’ll create a `browser` instance in playwright. From there we can open a specific `page` for each user. `adminPage` and `userPage` respectfully. Now for the fun part, each page/browser has `context` Playwright can keep track of and a `storageState` where all of this information can live until you want to call it again. Looking at our `adminPage` calls, you’ll see we have instructed Playwright to navigate to the `LOGIN_URL` and log the user in before saving anything to `storageState`. This means, when we go to use the state for admin, we’ll already have an Admin User signed in to our app, cookies saved, and in our case redirected back to the home page ready to go. In a `beforeEach` scenario, you might perform all these same steps minus the storageState save. The beauty here is I don’t have to worry about creating a `beforeEach` block for _**every**_ spec. This means: - Fewer DB hits - Faster Specs - Less maintenance and cleanup ## Test Example Having our Admin and User defined in the global setup means we can make use of `test.use({ storageState: 'file-path-to-storage' });` Our home page is a simple welcome message with the Users name. ```typescript // pages/index.tsx
{welcomeMsg}
``` ```typescript // tests/e2e/auth.spec.ts import { Page, test, expect } from '@playwright/test'; import { ADMIN, APP_URL, USER } from './constants'; test.describe(() => { test.use({ storageState: USER.storageState }); test('Can Login as a User', async ({ page }: { page: Page }) => { await page.goto(APP_URL); await page.waitForURL((url) => url.origin === APP_URL, { waitUntil: 'networkidle' }); await page.waitForSelector('internal:attr=[data-testid="welcome-header"]'); const welcomeHeader = await page.getByTestId('welcome-header'); const welcomeMsg = `Welcome, ${USER.firstName}!`; await expect(welcomeHeader).toContainText(welcomeMsg, { ignoreCase: true }); }); }); test.describe(() => { test.use({ storageState: ADMIN.storageState }); test('Can Login as an Admin', async ({ page }: { page: Page }) => { await page.goto(APP_URL); await page.waitForURL((url) => url.origin === APP_URL, { waitUntil: 'networkidle' }); await page.waitForSelector('internal:attr=[data-testid="welcome-header"]'); const welcomeHeader = await page.getByTestId('welcome-header'); const welcomeMsg = `Welcome, ${ADMIN.firstName}!`; await expect(welcomeHeader).toContainText(welcomeMsg, { ignoreCase: true }); }); }); ``` For each of these, we use the StorageState path for each user. Navigate back to the home page, and assert that we see our Welcome text. A quick run with `yarn playwright test` and 1… 2… 3… ✅ ## Conclusion From async/await, the ability to run factories, and having robust options when testing different scenarios, Playwright has been a joy to work with so far in comparison. While this strategy may not work for every scenario, I’m a lot more confident in the tools Playwright has provided to be successful long term. Stay tuned for a more in-depth comparison as we continue to build out. --- [View on echobind.com](https://echobind.com/post/playwright-with-next-auth) --- # React vs React Native: How Different Are They, Really? > Before building React Native applications, web developers and managers often ask just how much knowledge transfers over from React on the web. This article attempts to answer that question from a number of different perspectives. _By Mike Cavaliere · 2022-12-01_ Over the years that I’ve been working with React (circa 2014) and React Native (circa 2016), I’ve had everyone from engineers to CEOs to product people ask me some form of the question, **"What are the differences between React and React Native?"** Developers want to know how much they have to learn, managers want to know how much their team’s velocity will be affected, and what other considerations there are. TL;DR: **mobile application development** is a very different world from **browser-based application development**, so naturally developers will need to learn new tools, paradigms and techniques. Even though the core React API is the same, you're coding for smartphones and tablets instead of browsers, and those have a very different set of rules. In parallel, managers will have to provision some new services for their React Native projects to run effectively. But there’s a lot more to it; I encourage you to read on. ## Things that translate over well from React Web to React Native There’s a good amount of knowledge that _will_ translate for React web developers. Enough, in fact, that this is why React Native is such a compelling choice for web engineers to start working on mobile applications. Here are things that React developers will not have to learn from scratch when starting to work with React Native. ### Core React API and concepts A component is a component is a component. Class-based components and functional components operate the same way in React Native as on the web. You still should [think in React](https://beta.reactjs.org/learn/thinking-in-react) apply all of your React best practices like using [composition](https://reactjs.org/docs/composition-vs-inheritance.html) and [lifting state up](https://reactjs.org/docs/lifting-state-up.html). ### React architectural concepts State management, orchestrating HTTP requests, synchronizing client and server state: all these things will apply to React Native. The differences will only arise when the device layer comes into play (for example, if you have to store data on the client, [`window.localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) doesn’t exist, but [`AsyncStorage`](https://react-native-async-storage.github.io/async-storage/) does.) ### React performance optimization best practices Many of the JavaScript-based performance optimization techniques that are specific to React will still apply. Namely, the use of `React.memo()`, `useMemo()` and `useCallback()` operate identically. Anything related to build optimization will be different, and React Native requires some additional attention to optimization (more on that below). ### Third-party tools Many tools for monitoring, error reporting, analytics, etc. have SDKs that translate over well. I'm thinking Sentry, Datadog, MixPanel, others. ### Pure JavaScript libraries `npm` packages that don’t rely on native code or CSS will also work well in React Native. Yes you can still use your beloved `lodash`, `axios`, `react-hook-form` and React Query / Tanstack Query. State management libraries like Redux, MobX, XState, and so on are fine as well. ## Things that are quite different between React Web and React Native As you’ll notice, this section is a lot longer than the previous one 😁  React development principles will certainly translate over to native well; but it’s important to realize that React isn’t a magic bullet. Developers will still need to learn plenty about mobile development in general, and specifically how to do things well in RN. ### Hardware The biggest paradigm shift when going from React web to React Native is perhaps the obvious one: you’re now building apps for completely different devices that run in a completely different environment. Mobile hardware can do things browsers can't, so that's a perk. But it also means new learning. Push notifications, in-app purchases and haptics are things that are now natively supported and have their own APIs for. ### Layout and Styling React Native uses the [Yoga](https://yogalayout.com/) engine under the hood, which allows you to use CSS properties to layout your React Native UI in a way that translates really well. Layout in Yoga is limited to Flexbox and absolute/relative positioning, however; there is no CSS grid and no `display` attribute. This keeps things simpler and more performant, but if developers are accustomed to using other layout techniques on the web, they’ll need to adjust to this new limitation. React Native does come packaged with block-level ([``](https://reactnative.dev/docs/view-style-props) ) and inline ([``](https://reactnative.dev/docs/text-style-props)) which can be thought of to parallel `
` and `` in html. They support a fair bit of common CSS properties: borders, backgrounds, typography and so on are all in there. But as before, don’t expect fancier properties like `calc()` to be available. ### Library availability CSS-based UI libs don't make sense on mobile; your new options include [NativeBase](https://nativebase.io/), [React Native Elements](https://reactnativeelements.com/) and others). Some web-based UI libs do have RN siblings though - such as [React Native Material](https://www.react-native-material.com/) and [React Native Paper](https://reactnativepaper.com/) (for Material-UI), and [tailwind-rn](https://github.com/vadimdemedes/tailwind-rn) (for Tailwind). This just means new decisions to make, some learning, and new paradigms for how to use the new libs. ### Library Differences A great many React libraries do indeed work on React Native as well, which is a great perk for the platform. It’s worth noting that some will have slight configuration changes to make them work on React Native. This is understandable since the environments are different (DOM vs no DOM, hardware differences, and so on). [`react-hook-form`](https://react-hook-form.com/get-started#ReactNative), for example, uses `
` tags on the web. But since HTML doesn’t exist in React Native, `react-hook-form` requires developers to wrap their forms in a `` component, which does work in RN. ### Navigation As a user, you already know that moving around a smartphone app is completely different than on a web app. [`react-navigation`](https://reactnavigation.org/) is the most common navigation lib for mobile (runner-up: [`react-native-navigation`](https://wix.github.io/react-native-navigation/docs/before-you-start/)). Both are fairly different from [`react-router`](https://reactrouter.com/en/main), [Next.js's router](https://nextjs.org/docs/routing/introduction), and so on. Not only do developers have to learn new APIs for this, they’ll have to think differently to plan out complex navigation properly. It’s all very achievable, but I wouldn’t advise assuming that the effort is trivial. ### Development and developer experience (DX) Some development tools for the web are similar (Chrome debugger, mostly) to those for React Native, but other aspects are very different (working with the [metro bundler](https://facebook.github.io/metro/), using [Flipper](https://fbflipper.com/), AsyncStorage debugging, more). Some techniques developers will use are the same (breakpoints and console logging), but others are different (knowing when to restart the packager vs reinstall the app on device). Testing while you develop anything is crucial; the iOS simulator and Android emulators let you simulate a variety of devices. Naturally the iOS sims only run natively on a Mac, which is worthy of note, as this is a roadblock for any dev team that uses Windows or Linux and doesn’t have a Mac readily available. Dev machine setup is also more time-consuming, and can be quirky. Devs will need to have both XCode and Android Studio installed beforehand, so that builds will compile and emulators on both platforms exist for running code. `watchman` is also a prerequisite along with `rvm` (or `rbenv`) and CocoaPods — but it’s important to note that our beloved [Expo](https://expo.dev/) makes a lot of this _way_ easier. (It’s one of the many reasons we use it for all of [our React Native projects at Echobind](https://echobind.com/services/react-native-app-development).) Lastly, both the iOS simulator and Android emulators have limited or no support for certain features of smartphones: haptics, geolocation, app store purchases, and the camera, just to name a few. It’s best to have at minimum one real device for each operating system to properly test with while developing. ### Performance optimization Besides React optimization (which is arguably more necessary on mobile), developers will now have different hardware and a different internal architecture to work with. They’ll have to know about the [JS and UI threads](https://brooklinmyers.medium.com/react-native-understanding-threads-e026c7d62bb2), and different (and IMO more challenging) ways to avoid jank and prevent crashing. Memory and energy consumption are now even more important, since mobile devices are more limited in these areas. And they’ll have to use new performance analysis tools. Have a look at React Native’s [performance overview](https://reactnative.dev/docs/performance) page just to get a sense of the approach. The stakes are also higher for both devs and product teams on mobile - crashes and bad performance are huge killers of App Store reviews. ## Other React Native differences that engineering leads and managers should know about Besides all of the technical differences, development process is different for web vs mobile apps, so naturally it also will be for React vs React Native apps. So here are some of the process-related considerations for how your dev team’s workflow will be affected when transitioning from React web to mobile development. ### Deployments and releases You can deploy to prod without restriction on web; on mobile you have to deal with both app stores, which can be cumbersome and slow especially for the initial release/approval process. You have to fill out _**lots**_ of documentation before the first time you release to production. Even distributing a beta version of your app for non-developers to test, either for formal QA or for casual testing, can be a process. It’s not as easy as just deploying anymore; depending on your distribution method and which app store you’re working with (more so for iOS distributions than Google, FWIW) you may have to add each tester to the portal manually, then kickoff another build for them to be able to access it. We use tools to simplify all of this at Echobind (like [Expo Application Services](https://expo.dev/eas), [GitHub Actions](https://github.com/features/actions) and others), but we make sure to accommodate time for this in our project budgets since it can be non-trivial to set up. ### Testing and QA Unit testing with Jest and React)Testing Library is similar on React Native; developers use the same syntax (and the React Native version of React Testing Library), so there isn’t a huge paradigm shift there. Developers will have to mock out different third-party libraries on native, though, since unit test suites won’t be able to run native code. End-to-end testing is completely different on React Native, however. None of the Selenium-based E2E testing tools will work; neither will newer tools like Cypress or Playwright. You may have expected this - these are all DOM-based, and there’s no DOM in React Native. So instead developers will have to learn [Detox](https://wix.github.io/Detox/) or [Appium](https://appium.io/). Also since it’s mobile, you can’t avoid manual testing completely, no matter how much you automate. Plan to allocate time for testing setup in each project, since [TestFlight](https://developer.apple.com/testflight/) and [Play Store](https://support.google.com/googleplay/android-developer/answer/9845334?hl=en) configuration are non-trivial (especially the former). Once again, [EAS makes this easier](https://docs.expo.dev/build/internal-distribution/), so save yourself frustration and use it. And with every release, plan time to manually test each build on each platform. ### DevOps Continuous Integration/Continuous delivery are a different animal for React Native apps as well. You may end up making different choices on services and tools to use, and setting them up separately. Firstly, keep in mind that if you’re building for both Android and iOS, you’ll need to run your CI builds on a Mac (at least for the iOS builds). Which means that you’re gonna pay a LOT more for your build runs than on Linux boxes. Look at EC2’s cheapest mac instance compared to it’s cheapest Linux instance (which has more powerful hardware): ![Comparison of pricing for Linux vs Mac instances on Amazon EC2: a 6-core CPU is 790.59 USA per month on a Mac instance, compared to 327.77 USD for a Linux instance with 16 cores](https://cms.echobind.com/assets/6d538df3-7fd1-469b-9a6c-abaf3b03b3ab) And similarly, GitHub Actions’ per-minute pricing for Linux vs MacOS instances: ![Comparison of per-minute pricing for GitHub Actions Linux vs Mac instances: a 3-core Mac instance is 0.08 USD per minute vs 0.008 USD for a 2-core Linux instance, or 0.032 for an 8-core Linux instance](https://cms.echobind.com/assets/9cd62861-e2a6-410a-95e3-b6edf98d8237) A MacOS instance costs 2.5 times as much Linux instance with almost three times the processing power; but compared to a Linux instance with one less CPU core, the Mac instance costs 10 times as much (!!). That’s just a visual way to say that you’ll need to think through your CI setup carefully, for more reasons than one. At Echobind for example, we typically setup [Expo Application Services (EAS)](https://expo.dev/eas) and [GitHub Actions](https://github.com/features/actions) for our React Native client projects and internal projects. Builds get triggered by a PR merge to `main`; GitHub Actions runs JS-related automation (linting, typechecking, unit tests, etc), then hands off control to EAS to do the actual builds and releases. For us, this setup provides the a good balance of ease of setup, features, DX and reasonable costs. It also makes testing a lot easier since people can scan install builds to their phones just by scanning a QR code. But one CI setup doesn’t fit all projects, so choose yours carefully. --- This is a fairly thorough list of differences between React for the web and React Native; please keep all of these things in mind when starting to learn React Native (if you’re a developer) or transitioning your dev team to React Native (if you’re a manager). Planning accordingly will save you a lot of time and frustration, and reduce surprises. ***Learn more about our [React Native capabilities](/capabilities/react-native-app-development).*** --- [View on echobind.com](https://echobind.com/post/react-vs-react-native-how-different-are-they) --- # Keeping a Remote Team Connected with Echobind’s 2022 Summit > Our annual summit allows our team to build on our foundation of community and provides opportunities for new, shared experiences. _By Krystalyn Bauer · 2022-11-23_
Our annual summit allows our team to build on our foundation of community and provides opportunities for new, shared experiences. We find our summit to be invaluable for a few reasons. We get to reflect on the work we have been doing and align on where we want to iterate. We get to deepen our connections as teammates and colleagues in person. With our refreshed alignment and strengthened connections, we elevate our collective skills as a team. This year, we focused on communication and problem-solving skills, which are crucial for our remote team of software engineers, designers, and strategists. After a two-year hiatus from our summit, we were ready to make up for lost time. We set out from all over the U.S. to gather at C Lazy U Dude Ranch in Granby, CO. We had a blast doing all the ranch things, but the magic really sparked during our workshops where we got to dive deeper into communication and problem-solving. The discussions we had and the challenges we shared created invaluable takeaways to bring home to our daily work life. We went beyond just getting to know each other and learned how to support each other better at work. ![](https://cms.echobind.com/assets/17ba1a87-c458-4566-8da9-95056085f344) Our team had the chance to participate in activities like horseback riding, hiking, archery, scavenger hunt, and (a crowd favorite) our murder mystery dinner while also meeting our colleagues - some of us for the first time. The ranch provided a really fun setting for us to get out of our everyday routines and comfort zones and try new things. Some of us rode a horse for the first time and some of us learned that ax throwing is wildly therapeutic. ![](https://cms.echobind.com/assets/85bd2599-9b02-44e6-9b76-839da13a59e1) Our annual summit is one of the largest investments Echobind makes. While much of the ROI is intangible, we survey our team to measure their satisfaction and were thrilled to see 100% responded that our annual retreat was valuable and achieved the goal of fostering stronger relationships among colleagues. We can say with confidence that the experiences we had as a team were priceless. Our hope is that the magic stays palpable throughout the year. ![](https://cms.echobind.com/assets/c8f65ed9-a596-4091-afd2-67493b98918c) --- [View on echobind.com](https://echobind.com/post/keeping-a-remote-team-connected-with-summit) --- # How to Use Your iPhone as a Webcam > Did you know that Apple has enabled the iPhone to work as a webcam? But that’s not all. You can now use it in a new view Apple calls Desk View, which allows you to share your desk surface. _By Jacob Galito · 2022-11-17_ Did you know that Apple has enabled the iPhone to work as a webcam? But that’s not all. You can now use it in a new view Apple calls Desk View, which allows you to share your desk surface. ![](https://cms.echobind.com/assets/80ea888a-dfc7-427b-b410-e1da78155c75) ## **What could I use this for?** If you are training or teaching anyone on any streaming services that requires showing you go through a process, this might help provide more context. For example: - Teaching an online course and working through math problems - Illustrating or painting - Note taking and sharing with the group - UI/UX design—wire framing ## Setting up your devices Here are the initial set up steps for your operating systems: **after iOS 16 and MacOS Ventura are out of beta, all of these steps will probably not be needed and just skip to searching for “Desk View”. - **Sign in or sign up**—You’ll want to go to [Apple’s Beta Software Program ](https://beta.apple.com/sp/betaprogram)and sign up, if you haven’t already, or sign in (This should be the same email account you use for your Apple account.) - Next, you’ll want to select ‘enroll your mac’ - **Back up your computer**—You’ll be prompted to back up your computer or you can choose to move ahead - **Enroll your mac**—this will download the Mac OS Public Beta Profile .dmg file - **Update OS**—Once it’s downloaded, click on the Apple icon > System Preferences > and you should see the available Ventura OS update - **Repeat these steps on** **your iPhone**—While your computer is updating, go to your iPhone and go to [Apple’s Beta Software Program](https://beta.apple.com/sp/betaprogram/guide#ios) and click the iPhone tab ### Opening Desk View Simply do a search, either go to the search icon or press command + spacebar and search for desk view. After you launched it once, you can drag and drop it to the left side of your dock to make it easier to get to. ![](https://cms.echobind.com/assets/a68f93cc-6409-4439-b90d-744fc2df01aa) ![](https://cms.echobind.com/assets/dd4b706f-9ff1-48da-94e4-8d1dcac11a6c) ### Other things needed - A Magsafe iPhone case - A Magsafe Popsocket or - A phone stand - A small tripod ### Other useful tools - [**Hand Mirror app**](https://handmirror.app/)—desktop tool for checking if your camera is working (and if you have anything on you before a call!) - [**Krisp app**](https://krisp.ai/)—very handy tool for audio setup for video calls ### **Gotchas** You might discover that this app quits working if you’ve updated one device and not the other. So if you ever run into that issue, go to your device settings and ensure that you are up-to-date with the latest beta releases. --- [View on echobind.com](https://echobind.com/post/how-to-use-iphone-as-webcam) --- # Conditionally Render Fields Using React Hook Form > Learn to conditionally render fields using React Hook Form! View step by step examples or get all the information you need with a TL;DR. _By Preston Kelly · 2022-11-15_ How can you conditionally render fields using React Hook Form? Continue reading for a step by step guide on how I got there. I'll also highlight some common pitfalls you may encounter. To begin, I created a simple form with [Tailwind](https://tailwindcss.com/) styling. This form displays a first name input, submit button and a checkbox that checks to see if the user has a last name. ### TL;DR Use React Hook Form's built-in `watch` function to watch for changes to an input field. Next, use React’s `useEffect` hook to `register` and `unregister` the fields. ```typescript
``` When we're creating a new input, we'll want to register that field with React Hook Form using the `register` function. We'll spread the `register` function on the input and give it a name. This name will be something that we can reference later on when we're looking to conditionally remove the field. ### Handle Form Now, let’s register our fields and create a simple `onSubmit` function that will console log our form data. I’ve also included a `User` type that we’ll use to tell React Hook Form’s `useForm` function what type of data we’ll be including. ```typescript import { useForm } from "react-hook-form"; type User = { firstName: string; lastNameCheck: boolean; lastName: string; }; export const Form = () => { const { register, handleSubmit } = useForm(); const onSubmit = (data: User) => console.log(data); return ( <>
); }; ``` ### Create Variable Next, we'll create a variable using React Hook Form's built in `watch` function that will "watch" or keep track of field changes. This watch function give us updates along the way. For simplicity, we’ll name our variable `watchLastNameCheck`, and this variable will track any changes to our “lastNameCheck” checkbox. ```typescript const { register, handleSubmit, watch } = useForm(); const watchLastNameCheck = watch("lastNameCheck"); ``` If you enter “Hello” into the First Name input and click the submit button, you’ll see data for the first name and checkbox fields in the console. ![](https://cms.echobind.com/assets/899ce44a-65d3-4ec7-8ec0-dd75dbdbb1ba) If we were to click on the "Last Name?" checkbox, enter in “World” and click submit again, we'll see that our `lastName` field is now returning in the console. ![](https://cms.echobind.com/assets/066be05d-3310-460e-b48e-0b320b558c65) ### Possible Pitfall What happens if a user unchecks the checkbox? When a user unchecks the checkbox, we still see the `lastName` field outputting to the console and this is something we do not want! Returning empty or null fields can lead to errors down the road. We'll only want information from fields that we've entered data into and are visually rendered on the page. The solution? We'll add in a `useEffect` to check the `watchLastNameCheck` variable we setup earlier. We'll register that input only when we need it. When a user unchecks our checkbox, we'll “unregister” the field using the built in `unregister` function from React Hook Form. ```typescript const { register, handleSubmit, unregister, watch } = useForm(); const watchLastNameCheck = watch("lastNameCheck"); useEffect(() => { if (watchLastNameCheck) { register("lastName"); } else { unregister("lastName"); } }, [register, unregister, watchLastNameCheck]); ``` You'll see that when you now check and uncheck the checkbox, the console output only displays the information we need. The `useEffect` removes the `lastName` field from our form data if the checkbox is not checked. I’ve gone ahead and added a check to only display the `lastName` field when the checkbox is active. Full code: ```typescript import { useEffect } from "react"; import { useForm } from "react-hook-form"; type User = { firstName: string; lastNameCheck: boolean; lastName: string; }; export const Form = () => { const { register, handleSubmit, unregister, watch } = useForm(); const watchLastNameCheck = watch("lastNameCheck"); useEffect(() => { if (watchLastNameCheck) { register("lastName"); } else { unregister("lastName"); } }, [register, unregister, watchLastNameCheck]); const onSubmit = (data: User) => console.log(data); return ( <>
{watchLastNameCheck ? ( ) : null}
); }; ``` --- [View on echobind.com](https://echobind.com/post/conditionally-render-fields-using-react-hook-form) --- # Why we ditched GraphQL for tRPC > While we normally use GraphQL to get data from the server to the client, tRPC provides a simpler, more streamlined experience for our developers, helping us make better apps for clients. _By Alex Anderson · 2022-11-08_ At Echobind, we’re committed to building the best software we can for our clients. As we choose our technology stack, we have to balance a number of tradeoffs, including stability, flexibility, scalability, and the speed of development. We wrap our favorite tools in a starter repository we call [Bison](https://github.com/echobind/bisonapp). We adopted GraphQL in Bison to give our apps end-to-end type checking, with type safety from the database all the way to the UI. It’s served us well, and has been the API layer for dozens of apps developed by Echobind. We’re always looking for ways to improve our processes, so when we noticed [tRPC](https://trpc.io) starting to become popular, we decided we would take a look. What we saw impressed us, so we’ve decided to adopt tRPC as the official API layer for Bison. > We've made the change to tRPC in the `canary` branch, which means it isn't fully released yet. If you want to see what it took to make this migration in Bison, take a look at [this commit](https://github.com/echobind/bisonapp/commit/60899f889344e547a2d3e680de97b20a0a754410) ### What is tRPC? tRPC and GraphQL serve the same purpose: Getting data from the server to the client. GraphQL is a specification which allows the client to request specific data, which the server resolves into a response with just the fields that were requested. tRPC, on the other hand, lets the client call server-defined procedures, passing along any relevant inputs and getting back a response. Inputs are type checked at runtime using validator libraries like [Zod](https://github.com/colinhacks/zod), and the types of the procedures are inferred from the server to the client. While you can make direct `fetch` calls to tRPC’s API, it includes a wrapper around [React Query](https://tanstack.com/query/v4/), a caching layer that provides an excellent user and developer experience. Both GraphQL and tRPC are perfectly compatible with React and React Native and have first-party client-side library support. Both support end-to-end type checking, with GraphQL requiring a codegen step. And both are fast enough for our purposes. Here’s what that looks like in practice, with mostly equivalent examples for both GraphQL and tRPC. First, let's look at GraphQL, both defining the resolver and fetching on the client (excluding any generated code): ```typescript // On the server export const User = objectType({ name: 'User', description: 'A User', definition(t) { t.nonNull.id('id'); t.nonNull.date('createdAt'); t.nonNull.date('updatedAt'); t.nonNull.list.nonNull.field('roles', { type: 'Role' }); // Show email as null for unauthorized users t.string('email', { resolve: (profile, _args, ctx) => (canAccess(profile, ctx) ? profile.email : null), }); }, }); export const UserRole = enumType({ name: 'Role', members: Object.values(Role), }); export const UserWhereUniqueInput = inputObjectType({ name: 'UserWhereUniqueInput', description: 'Input to find users based on unique fields', definition(t) { t.id('id'); t.string('email'); }, }); export const findUniqueUserQuery = queryField('user', { type: 'User', args: { where: nonNull(arg({ type: 'UserWhereUniqueInput' })), }, resolve: async (_root, args, ctx) => { return await ctx.db.user.findUnique({ where: prismaArgObject(args.where) }); }, }); // On the client export const QUERY = gql` query User($id:ID!) { user(id:$id) { name } } `; export const UserCell = ({userId}:{userId:string}) => { const { data, loading, error } = useUserQuery({ variables: { where: { id: userId }, }, }); // ... } ``` And then the same, but using tRPC instead: ```typescript // On the server export const defaultUserSelect = Prisma.validator()({ id: true, email: true, createdAt: true, updatedAt: true, roles: true, profile: { select: { firstName: true, lastName: true } }, }); export const userRouter = t.router({ find: t.procedure .input( z.object({ id: z.string().optional(), email: z.string().optional() }) ) .query(async ({ ctx, input }) => { const user = await ctx.db.user.findUniqueOrThrow({ where: input, select: defaultUserSelect }); if (!isAdmin(ctx.user) && user.id !== ctx.user?.id) { return { ...user, email: null }; } return user; }), }); // On the client export const UserCell = ({userId}:{userId:string}) => { const { data, loading, error } = trpc.user.find.useQuery({id:userId}) // ... } ``` The difference between these two examples highlights the initial reason we switched from GraphQL to tRPC. ### Less Boilerplate You’ll notice in the code samples above, tRPC is able to do the same work with much fewer lines of code. In fact, when we migrated Bison to tRPC, we added 1,765 lines of code while _removing_ 3,373 - a net change of 1,608 lines of code! This is partially because GraphQL famously has the [“Double declaration problem”](https://wundergraph.com/blog/solving_the_double_quintuple_declaration_problem_in_graphql_applications_how_to_not_repeat_yourself) - there’s a lot of repeating yourself, especially if you’re using TypeScript. Tools like GraphQL codegen help with this, but it’s still a _lot_. 1. Define the database schema (Prisma) 2. Define the API schema (Nexus) 3. Write a GraphQL Operation (gql) 4. Define the response type definition (GraphQL codegen) 5. Define the query (Apollo Client) tRPC trims this down significantly. 1. Define the database schema (Prisma) 2. Define the procedure (tRPC router) 3. Define the query (tRPC client/React Query) The simplicity speaks for itself. ### Avoiding Code Generation In both cases, we can achieve end-to-end type safety, which is table-stakes for our apps, but the method of achieving this type safety is much more complicated with GraphQL, requiring three layers of code-generation: - [Prisma](https://www.prisma.io) generates types from our database schema. - [Nexus](https://nexusjs.org) generates types from our database schema. - [GraphQL Codegen](https://www.the-guild.dev/graphql/codegen) generates frontend types and React hooks from our GraphQL request definitions. This code generation doesn’t come without its consequences. For one app we built at Echobind, Nexus generates a 2000 line type file while GraphQL codegen generates an 8200 _dense_ type file. All of that takes a long time for the type checker to parse and check, both when running `tsc` and in VSCode. We often need to restart our VSCode language server because it gets bogged down with all of the type checking it has to do. tRPC instead relies on one very large assumption: Your server is written in TypeScript, and is colocated with the client code. tRPC has you define a server-side router for your procedures, then export the type of that which Typescript automatically infers, and then import that type to be used on the client-side. Since types are automatically removed when Typescript code is compiled, there is no extra code added to the client bundle, and no extra types to slow down the type checker. Just type safety. ### Client Bundle Size It’s no secret - the less JavaScript you ship to your users, the better their experience will be. To build great software, you need _some_ JavaScript, but if an alternative comes along that uses _less_ JavaScript, it’s worth a second look. Let’s compare the client-side dependencies needed for both approaches. These numbers were calculated by putting the packages through [bundlephobia.com](http://bundlephobia.com) and tracking the “minified + gzip” sizes. | **GraphQL** | **tRPC** | | --------------------------- | ----------------------------- | | @apollo/client: 40kb | @trpc/client: 4.4kb | | apollo-upload-client: 1.5kb | @trpc/react-query: 1.8kb | | graphql: 39.7kb | @tanstack/react-query: 12.7kb | | | @trpc/next: 4.8kb | | **TOTAL: 81.2kb** | **TOTAL: 23.7kb** | The GraphQL bundle is almost 3.5 times the size of tRPC, which means that much more loading time for sites that use it. > As an aside, we could still get the bundle size savings while using GraphQL by combining React Query with a GraphQL request library like the conveniently-named `graphql-request` (7.9kb). ### React Query & Cache Management A frequent bug we’ve run into building with Apollo Client has to do with updating the cache after a mutation. If you create your queries right, and if your mutations return the correct data, mutations are supposed to automatically update its fancy normalized cache. But that’s rarely how it works out in practice. This could be a classic case of “you’re holding it wrong”, except it’s not clear what the correct solution is. Do we need to use `refetchQueries`? That gets kind of messy depending on which variables are being used for the query. Maybe we should use `writeQuery` or `writeFragment`? That has the same pitfalls, plus we have to write a bunch of extra code to surgically alter the cache. It shouldn’t be this hard to keep the cache up to date after a request. It’s impossible to overstate how many nice things React Query provides out of the box. Compared to Apollo Client’s incredibly complicated normalized cache, React Query is a walk in the park. tRPC simplifies it further by keying the cache entries to each procedure. Triggering a refetch is as easy as running `utils.user.find.invalidate({ id: user.id })`. Along with it comes easy mutation handling, simple patterns for optimistic updates (along with rollbacks in case there are errors), and heuristics for automatically refetching data so it stays fresh. Combined with tRPC’s type safety, React Query is the most delightful way to fetch data on the client. ### IDE Improvements The methods tRPC 10 uses for type checking have some surprising and convenient side effects. Since the frontend queries are type checked based on the backend procedures, VS Code’s “Go To Definition” feature works _across the network boundary_. I can click on a procedure definition in a client-side file and it will take me right to where that procedure is defined on the server-side.
It gets better. Not only can we jump to the file, if we use VS Codes “Rename All Instances” on an input or procedure name, those changes also propagate between the server and client.
Even with the fancy GraphQL codegen, this just isn’t possible with the way GraphQL works. Having these tools right in the IDE is a huge win for convenience and productivity. ## What did we lose? Very rarely does a major change like this not have downsides, and leaving GraphQL is no exception. As we’ve made the jump, there are a handful of things GraphQL provided for free that require a bit more effort using tRPC. ### Field Requests The most obvious is that the client doesn’t get to pick which fields it fetches, whereas GraphQL allows the client to define exactly the fields it wants for any request. There are two workarounds. First, a tRPC procedure could be defined with an input where the client can request certain fields. This might take a bit of finagling to get the input and output types to line up, but would provide a similar experience to GraphQL. However, I think a better option is to embrace the simplicity of having the client accept whatever the server returns. In my experience, over-fetching is not as big of a problem as many people make it out to be. If there is a particular procedure that returns far too much or not enough data, there’s nothing wrong with creating a second procedure that returns data more closely scoped to what the client needs. ### Field Resolvers Another huge benefit of GraphQL is the ability to create custom resolvers on a field-by-field basis. The default is to just pass through whatever value the parent resolver provided for that field, but GraphQL makes it really easy to combine and transform fields, even using arguments from the request as part of the transformation. Common examples include a virtual `fullName` field which combines the `firstName` and `lastName` fields, or a `date` field which lets the user choose what format they want for the date. This is another case where tRPC can get around the limitation either by using an input or creating a new procedure to provide the transformed data. Probably my biggest lesson in using tRPC: Creating a new procedure is cheap. So long as they are named well, having many procedures is not a bad thing. ### Introspection and Documentation GraphQL is famous for its self-documenting API. As part of the spec, GraphQL servers can publish an introspection query, which lets anyone see what objects, queries, and mutations the server supports. It’s great for visibility and learning what the API supports. tRPC has no such introspection query. In fact, it isn’t great for creating a public facing API. The types themselves work great for building first-party apps, but if you want to open your API up to third parties, you’ll have to create your own documentation. There is an [OpenAPI Extension](https://github.com/jlalmes/trpc-openapi) for tRPC that can be used to create a more REST-like API from your procedures, and that in turn can be used for auto-generating documentation. But if my app needed to offer third-party API access, I would likely reach for GraphQL again. ### Colocated TypeScript Only One of the best things about GraphQL is that it isn’t actually a technology, it’s just a specification. That means anyone can create a GraphQL server or in any language. So long as it matches the spec, it will work across whatever platform it's used on, whether it be Ruby, JavaScript, Elixir, or Python. That’s part of the reason GraphQL codegen is so valuable. It allows schema definitions and types to be generated regardless of the backend that hosts the GraphQL server. The same can’t be said for tRPC. Its type checking only works if the server is written in TypeScript, since the client needs those TypeScript types to enjoy its type safety. It’s possible in the future that servers that implement the tRPC HTTP spec could be written in other languages, but they would require a codegen process to make the proper types available to the client. And, if your tRPC server is located in a different repository, you’ll have to figure out a way to get the generated types into your client app to enjoy the type safety. tRPC works best when the client and server are either part of the same app, like with Next.js, or part of a monorepo. ## Conclusion tRPC has proven to be exactly the tool that Echobind needs for building our client’s apps. GraphQL may be powerful and capable, but the boilerplate and complexity it requires often slows us down and makes it difficult to build robust, responsive sites. With tRPC, we hope to double down on our commitment to building the best possible software we can for ourselves and our clients. --- [View on echobind.com](https://echobind.com/post/why-we-ditched-graphql-for-trpc) --- # Book Review: Ask Your Developer by Jeff Lawson > A true TL;DR of Ask Your Developer, by Jeff Lawson. Claire shares her favorite takeaways from this great read. _By Claire Surma · 2022-11-03_ **tl;dr** A quick summary of three main themes in *Ask Your Developer: How to Harness the Power of Software Developers and Win in the 21st Century*, by Jeff Lawson. ![](https://cms.echobind.com/assets/1d930968-a4e6-4d9c-b122-eae3e25023ab) ★★★★ Jeff Lawson is a lifelong software developer and serial entrepreneur, turned co-founder and CEO of Twilio. In his book, ‘Ask Your Developer’, Jeff recounts his windy road to founding Twilio in 2008 and how it grew into a multi-billion dollar company and then became a publicly traded company in eight years. Without getting too into the tech weeds, he shares how AWS works based on his early experience at Amazon and how Twilio simplifies customer communications. But ‘Ask Your Developer's’ real "meat and potatoes" is Jeff's rich storytelling and real-life examples of how a culture of continuous experimentation and creativity paired with open communication between developers and customers yields more successful products and both happier employees and end users. As a strategy team member at Echobind, a software development agency, here are my three favorite takeaways from the book. **Innovation = Experimentation + Iteration** Jeff encourages teams to shed the traditional rigid company culture where mistakes get punished and instead celebrate on-the-job learning and testing. And with that added on, to be comfortable with "disproving hypotheses," or in other words, "failure." Jeff sees code as a creative medium and developers as creative problem solvers who probably got into engineering because they like consistent new challenges that allow them to learn and expand horizons. Everyone benefits when engineers are presented with the customer's problem (not a list of requirements) and asked to experiment with their creativity to build a solution within existing safeguards — time restraints, data structures, code paths, etc. **A Culture of Small Teams is Key** Beyond an open learning environment woven into the development process and supported top-down, Jeff also values a culture of unity and collaboration across departments. The full small project team involved in building the product — developers, creative, customer service, etc. — should meet regularly to test and review progress together. Doing this together allows for early detection of blockers, pivot needs, and holistic solutions. By encouraging a culture of agile flexibility and small project team autonomy keeps everyone on a project connected to the problem and accountable to the customer. Jeff reminds us that the best solutions don’t always come from the loudest or highest-ranked person, and when small teams are valued, quieter voices can be heard. **Customers and Engineers Should Be Communicating, Early and Often** A customer-centric company consistently visits and re-visits customers' needs in relation to its outputs and self-corrects when there is a disconnect. Throughout the book, Jeff reiterates this importance by having customers share the problems they are trying to solve via software directly with the engineers and for developers to remain in the customer feedback loop throughout the development lifecycle. Developers are solving real-life problems, and Jeff thinks it is essential for them to know the humans behind the problems and the nuanced depth and _whys_ of the issue. Humanizing the customer and their problem will bring purpose to the developer's work and heighten their instincts when making effort versus expected value estimations and quick decisions that impact product features and functions. Jeff’s relatable tone and storytelling make ‘Ask Your Developer’ an easy choice for the non-coder wanting to read a book about software. If you work closely with software developers, this book is a good starting point on strategic ways you can better support engineers, the development process, and the end product. **Resources** - [Official Website](https://www.askyourdeveloper.com/) --- [View on echobind.com](https://echobind.com/post/ask-your-developer-book-review) --- # What’s the difference between artifacts and cache in GitHub Actions? > When should you use artifacts and when should you use Cache in @ GitHub Actions? Our lead software developer Cully Larson provides some of the best use cases for each. _By Cully Larson · 2022-10-27_ GitHub Actions has a couple of ways to store files: artifacts and cache. They have enough functional overlap that it can be difficult to decide which is best to use. Rather than look at the capabilities of these actions (there are enough articles out there on that), let’s look at situations where you would want to use each one. ## Tl;dr - Use artifacts if you want to share files between jobs in the **same workflow** or view/download files after a workflow has been completed. - Use cache if you want to share files **between workflows** (e.g. between the workflows run by multiple PRs or commits). ## Artifacts Artifacts essentially allow you to do two things: store files that will persist after a job is completed, or share files with another job in the same workflow. You would use artifacts if you want to: - Access files after a job has been completed. For example: logs, test coverage, screen recordings of e2e tests, etc. GitHub Actions will allow you to view or download these files. - Produce files in one job and use them in another job in the same workflow. This is the more interesting use case and the one that can seem to have some overlap with the cache action. An example of using artifacts to share files between jobs is a project build step. Let’s say you’re using GitHub Actions to test your application and you have unit tests, API tests, and E2E tests. Each of these test suites needs its own environment and can run in parallel as long as those environments are isolated. So you decide to split your workflow into three jobs: 1. Unit tests 2. API tests 3. E2E tests As you set up this workflow, you realize that each job needs to build the application. So why not create another job for the build step and then share the build between the other jobs? You can do this with artifacts. ![](https://cms.echobind.com/assets/23ae19ac-c378-4bcb-8054-8018d365f449) The “Project Build” job stores the build files as an artifact and the other jobs download the build files before running. That way the build only happens once. You can do this with cache as well. However, cache files are shared between workflows (not just within a single workflow), you would need to know that the files used to build your project have not changed. This can be a challenge and might not be something you want anyway. Since artifacts are only accessible within a specific workflow, you don’t need to worry about other workflows grabbing them by accident. ## Cache GitHub Actions cache allows you to store files that can be used between workflows. It is recommended for files that won’t change often, or at least won’t change on every run of the workflow. However, if you can think of a reason to share files between workflows, even if they change on every run, then cache is the way to go. You can even use it to share files between jobs in the same workflow. Though, as we’ve seen, artifacts are likely going to work better for that. The frequently cited use case for cache is storing project dependencies (e.g. npm, pip, Gradle, etc.). These may not change frequently between PRs/commits. So caching them and sharing them between workflows (i.e. between the runs from PRs and commits) makes sense. But you can be more creative with cache if it solves your problem. For example, if you need to download a large database that doesn’t change frequently between builds, you can use cache to store it. Basically, as long as you need to access some files between workflows, use cache. ## Resources - [https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts](https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts) - [https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) --- [View on echobind.com](https://echobind.com/post/difference-between-artifacts-and-cache-in-GitHub-Actions) --- # How Businesses Benefit from Integrating with Stripe > Not only can Stripe help a company accept various payment methods, it can enable recurring payments, invoicing, decrease fraud, and overall optimize revenue. _By Claire Surma · 2022-10-12_ Our team is consistently exploring new ways we can better support our customer’s big ideas, expand their reach, and grow their revenue streams. For that reason, Echobind recently became a [Stripe](https://stripe.com) consulting partner, getting a number of our people trained and certified in Stripe’s products. ## What is Stripe? Stripe is a platform for payments infrastructure. In its simplest form, the Stripe platform enables businesses to accept various payment methods — credit or debit cards, mobile wallets, and buy now / pay later services — and securely transfer customer funds into their business account(s). But Stripe also does a whole lot more! It can enable apps to accept recurring payments, handle invoicing easily, decrease fraud, refund charges, and overall optimize a company’s revenue. ## Why should anyone care about payments processing? The most recent [Census Bureau's Annual Retail Trade Survey](https://www.census.gov/programs-surveys/arts.html) reported e-commerce sales increased by $244.2 billion, or 43%, in the first year of the pandemic. With this continued e-commerce boom ballooning online transactions, it is important for businesses to safely position themselves in the digital economy in order to participate in customers’ purchasing and spending trends. This is why our Echobind team has dedicated time and resources to becoming experts in developing and implementing online payment solutions. ## Who Uses Stripe? - As a customer, you have definitely paid a business via Stripe products - eCommerce companies accepting one-time customer payments, like [Papier](https://stripe.com/en-hu/customers/papier) - B2B SaaS companies accepting recurring payments, like [Slack](https://stripe.com/customers/slack) - Marketplace or SaaS Platforms accepting one-to-one or one-to-many payments, like [InstaCart](https://stripe.com/customers/instacart) or [Shopify](https://stripe.com/customers/shopify) - Online content creators - Individuals with small businesses all the way to large Fortune 500 enterprises ## Why Use Stripe? Numbers sometimes speak louder than words. Here are a few stats on why many companies use Stripe’s platform: - increases cart conversion rates by 35% - reduces fraud by 32% - accepts 135+ currencies in 47+ countries Here are some features of the Stripe platform: - A seamless customer checkout experience - Collects, routes, and pays money globally - Flexible pricing options (per seat, fixed fee, or usage-based) - Accepts coupons, free trials, prorations, and overages - Comprehensive financial health reporting - Third-party systems (ERPs and CRMs) integration - Compliance requirement guides - Security features that encrypt and protect financial information Any business processing payments can integrate with Stripe products to improve their checkout experience, reduce fraudulent charges, and accept more forms of payment online. Our Echobind team members from Engineering, Strategy, and Community and Partnerships received comprehensive training on Stripe products to become Stripe Practitioner, Implementation Architect, and Developer Certified. This means we have a team of experts ready and available to help in advising on payment processing needs, implementing eCommerce payment solutions, and integrating Stripe APIs into apps and platforms for our clients. _**(To learn more about how Echobind works with Stripe, visit to our [Stripe partner page](/partners/stripe))**_ --- [View on echobind.com](https://echobind.com/post/how-businesses-benefit-from-integrating-with-stripe) --- # Export Multiple Frames in Figma to a Single Multi-Page PDF > When you export multiple frames using Figma’s asset export, the resulting output is a separate PDF file for each slide. Here’s how to export a single, multi-page PDF in Figma. _By Kaila VanSumer · 2022-10-10_ At Echobind we make _a lot_ of slide deck presentations. Our preferred design tool for everything is Figma, but when you export multiple frames using Figma’s traditional asset export option in the design panel on the right, the resulting output is a separate PDF file for each of your slides. This is fine if you want to spend extra time combining your slides together in Preview and making sure they’re in the right order after you’ve exported them, but for those of us who have better things to do there’s a much easier way to export your slide deck into a single, multi-page PDF. Here’s how: ## Step 1: Make sure all your frames are 1) on the same page and 2) in order from left to right, top to bottom. This is how Figma will read the order and will ensure all your slides are ordered correctly once they’re exported. ![](https://cms.echobind.com/assets/4181b7d2-c69e-419d-aa32-d2afbd07d287) ## Step 2: Next, select your frames by clicking and dragging on the canvas to highlight all the frames. You can also hold `shift` and click each frame to select if that’s easier. Note: the order in which you select the frames will not affect how the slides are ordered once they’re exported; that’s determined by how the frames are arranged in Step 1. ![](https://cms.echobind.com/assets/a13746e0-c9e2-4d01-8790-19d62645bc66) ## Step 3: With your frames selected, click on the Figma icon dropdown in the top left corner of the screen. Hover over `File` and then click `Export frames to PDF…`. Name and save your file, and you’re done! ![](https://cms.echobind.com/assets/20cf7658-00e0-417b-8c7a-cec5d4942ad0) **Bonus** If your PDF is too large to send via email, you can reduce the file size in Adobe Acrobat or Preview. This is usually found under `File` and the command will be something like `Reduce file size` or `Compress PDF`. Alternatively, you can use Adobe’s free [PDF compression tool](https://www.adobe.com/acrobat/online/compress-pdf.html). --- [View on echobind.com](https://echobind.com/post/export-a-pdf-from-figma) --- # Why Our Team is Stripe Certified; Yes, Including Non-Developers > Turning our team members into Stripe Certified Developers and Implementation Architects helps us help our clients. _By Mike Cavaliere · 2022-10-04_ Stripe’s incredibly robust ecosystem allows our clients to accept more payments in more elaborate and customized ways than ever. And software agencies like us can quickly implement these complex solutions with an ease that did not exist a decade ago. Turning our team members into Stripe Certified Developers and Stripe Certified Implementation Architects helps us further that goal. ## Stripe is Powerful…Projects are Complex Stripe's APIs themselves—from Checkout to Billing to Payments—are incredibly intuitive and a joy to work with. Our clients’ business models can be much more complex. Take, for example, one of our projects in the real estate space, a mobile app. The owners of the app needed to allow their customers to subscribe to multiple services provided by different businesses as a bundled subscription so that users could make a single payment. After each successful payment, the submitted amount would be split and transferred into appropriate Connect accounts for every service provider. Each Stripe API we used—Connect, Elements, and Billing—is straightforward and extremely well-documented. But to implement a solution like the above one, each developer needs to know the capabilities of Stripe’s tools so that planning the architecture is less risky and implementation goes smoothly. ## What Stripe Developers Learn Being certified gives our engineers access to thorough training resources and testing provided by Stripe. After undergoing Stripe Developer training, our engineers have a more solid understanding of: - Which Stripe product to use for different use cases - Internal Stripe APIs, limitations, and capabilities - All about the payments industry: credit processors, fraud, chargebacks, etc - What happens behind the scenes when payments are accepted: the full lifecycle of a charge And since it’s developer-specific training, they also learn: - Technical best practices - Project launch best practices - How to thoroughly use Stripe’s UI tools in development, testing, and production ## What Stripe Implementation Architects Learn Our employees who have received Stripe Implementation Architect certification get a similarly deep understanding of the Stripe platform, its products, and the industry. They also work hands-on with Stripe’s APIs during training and on projects. But in addition, they receive training and testing on the specifics of: - Architecting large-scale, complex payment flows with Stripe products - Project planning for complex Stripe implementation projects - Strategies for determining business needs and choosing approaches accordingly - Lots more tools for proposing and executing the right solution for the project ## What Non-Developers Learn about Stripe Others in our organization have also undergone Stripe Sales Training or general Stripe product training and business fundamentals. This keeps them knowledgeable about things that are very important for our customers, such as common payment needs of different business models (SaaS apps, marketplaces, eCommerce apps), and a good understanding of Stripe products, just like the developers and architects. By having even non-developers trained on these things, everyone working on a Stripe implementation project can work together to ensure that we’re taking the right approach at all times. ## Knowledge is Power…and Quality We are always looking for ways our team members can remain a few steps ahead when it comes to technical and strategic expertise. After training like this, our engineers have a more solid knowledge of the industry and finer details of all things Stripe. That level of understanding, combined with their hands-on experience implementing payments solutions, means that: - they’ll ship code that is rock-solid - they’ll be more likely to spot risks in project planning - they’ll solve technical problems more wisely And for our architects: - they’ll account for all stakeholders and business goals when doing discovery - they’ll propose the most comprehensive solution that targets all of those goals using Stripe products efficiently - they’ll work with our developers to execute it to perfection The end result: solving the right problem, with the right tools, in the right timeline - providing the right solution for our clients. ## Doin’ it (Stripe) Well As we specialize in software solutions that integrate Stripe payment processing products, it is important that our whole team — both code-facing and client-facing members — is confident in their Stripe expertise. Having a qualified, Stripe-certified team offers the highest value to our customers. This allows us to provide better and faster service when advising on payment processing needs, building and implementing Stripe payment solutions, and integrating Stripe APIs to brand specifications. _**(To learn more about how Echobind works with Stripe, visit to our [Stripe partner page](/partners/stripe))**_ --- [View on echobind.com](https://echobind.com/post/why-our-team-is-stripe-certified) --- # Slicing TypeScript Literal Strings > Using TypeScript’s literal inference, we can pull specific segments out of literal strings. This can help reuse types and keep our code DRY. _By Alex Anderson · 2022-09-29_ Template literal strings are a powerful feature of TypeScript that makes it easy to create and modify literal string types. In this post, we’ll look at the syntax TypeScript provides for both adding and removing parts of a literal string type. This is especially handy when creating reusable types to dynamically access object keys. > You can see the types used in this post in [this TypeScript playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZgJwK4EsoGERLFCMC8MA3jDAFCmkCGADjQDYCmAXAIwA0pFlARlWPyrMArJ3KUYIBPwDmLAMxkAvmTJQAnjUbxkaANKN1BGAGtDIODA1aLO1Biw4IAblXXtie5mxQDRwgAGACREnvqGSt44Aa4A9LESpAB6APxumtoAmgAiKAiMwFAo4HiEAERQIDRlMAA+MGXAjDiMCDX1ZTwgUJUAtmWu7jAAGrn5hcWQxmVMcFDtDU0tbXUNCCgyABbzgxkwAAogEGiTxsFEOXkFRSVKALQho1cTtzFk8YkwqelaMEh0rWAVAgjAAYroHD4-GcQgBVAEIIEggA8YV8hgAfJFHFAAu8Ep8vmk1HsIPQUE0ACbg+zQwhoqLooyMAAeUGalLw5xQYDgrTsaAAclReoxsT4AjAUgKoMLRTBmDAwIwAG6tOIEiTfACQZFAkFgjF6VBQ9AAgpTKfkIKUGlR+JTWhBwAABAqbEA8HmUgB0oH6u1+RpN9GyIGNPOMZG17lswdNFqtjBtMFZ7LAnJg52g6zAMiULpCPL5CBglPDJrASh9IRzPPzkuj0vLEbA5G1iuVaoQziAA). ### Literal String Types Most string types can represent _any_ string, the same way that a JavaScript string value could contain any string. ```typescript let fruitName:string = "apple"; fruitName = "banana"; ``` But sometimes a particular value should only contain _certain_ strings. A common example is a value that references the keys of an object. For that, we put the exact values the string can be in a union of [literal string types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). ```typescript const fruitCounts = { apple:1, banana:5, orange:3} type fruitKey = "apple" | "banana" | "orange"; ``` > An easier way to get the keys of an object is using `typeof` to get the type of the object and `keyof` to get the keys of that object type. It has the same effect as the manual list we wrote out above. There are often situations where the literal types don’t match how the values are used. Suppose we had our `fruitKey` type, but instead of matching those keys exactly, the `fruitCounts` object had slightly different keys. ```typescript const fruitCounts = { appleCount:1, bananaCount:1, orangeCount:1 } ``` Rewriting our `fruitKey` list would be annoying. What if, instead, TypeScript could automatically rewrite those keys to match a new pattern? ### Template Literal Types TypeScript provides that functionality through [template literal types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). They look just like JavaScript’s template literal strings - you can even interpolate other types in the middle of the literal string - but instead of string values, they transform literal string types. ```typescript type fruitCountKey = `${fruitKey}Count`; // type fruitCountKey = "appleCount" | "bananaCount" | "orangeCount" ``` > Notice how the template literal type operates on each of the items in the type union, distributing the transform across all of them. One thing to bear in mind is the computational complexity of generating these types. Combining a union of three types with a static string will yield a union of three types, but template literal types let you combine multiple unions together that would include every permutation. ```typescript type YDirections = "top" | "center" | "bottom"; type XDirections = "left" | "center" | "right"; type Position = `${YDirections}-${XDirections}`; // type Position = "top-center" | "top-left" | "top-right" ... [6 more] ``` > The type above only generated 9 results, but adding more terms would increase that exponentially. At a certain point, it will give up and throw an error, but even before that point it can slow down your type checking as TypeScript generates all the possible options. Sometimes, it’s probably best to stick with `string`. TypeScript [includes helpers ](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#intrinsic-string-manipulation-types)for adjusting the capitalization of literal strings too. They include `Uppercase<>`, `Lowercase<>`, `Capitalize<>`, `Uncapitalize<>`. ```typescript type uppercaseFruitCountKey = `${Uppercase}Count` // type fruitCountKey = "APPLECount" | "BANANACount" | "ORANGECount" ``` Suppose we were going the other way though - we had an object with keys for `appleCount`, etc. and we wanted to _remove_ `Count` from the string literal. For that, we combine template literal types with conditional types. ### Conditional Types & Inference [Conditional types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) are a more advanced feature of TypeScript that let you change a type based on a condition. In essence, a conditional type says “If this type _extends_ (or matches) this other type, then replace it with that type. Otherwise, replace it with yet another type.” For example, here’s a conditional type that removes `null` and `undefined` from a type union by replacing them with `never`. ```typescript type NonNullable = Input extends null | undefined ? never : Input ``` This type says “If `Input` is either `null` or `undefined`, replace it with `never` (which removes it from the union). Otherwise, keep `Input` in the union.” [Conditional ](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types)[_inference_](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types) takes it a step further, by tapping into TypeScript’s inference system to pull types out of other types. We can use it to unwrap a `Promise<>` type to get at the resolved value. ```typescript type UnwrapPromise = Input extends Promise ? ResolvedType : Input; ``` This type says “If `Input` is a `Promise<>` type, infer what the resolved type of that promise is (the type inside the brackets of `Promise<>`), pull it out into its own type called `ResolvedType`, and have that be the final type. Otherwise, have the final type be `Input`.” The `infer` keyword works for more than just generic `Promise<>` types too. In fact, it can be used to pull out function parameters, return values, and, yes, even parts of a template literal string. ### Template Literal Inference Remember, we have a union of literal strings that looks like this. ```typescript type fruitCountKey = "appleCount" | "bananaCount" | "orangeCount" ``` We want to remove the “Count” from each of those strings, leaving us with just the fruit names. Or in other words, we want to pull the fruit name out into a new type. What we can do is create a template literal type that matches the `${fruitName}Count` pattern, and use `infer` to pull the fruit name into a type. ```typescript type fruitKey = fruitCountKey extends `${infer fruitName}Count` ? fruitName : never; // type fruitKey = "apple" | "banana" | "orange"; ``` This type says “If fruitCountKey matches the pattern `${fruitName}Count`, meaning it ends with ’Count’, then grab that first part of the string, put it in a `fruitName` type, and have that be the final type. Otherwise, return never, which removes that item from the union entirely.” And, of course, this can be be used with even more complicated patterns combining literal types, string types, and inference - such as this one which successfully pulls the domain name out of an email address. ```typescript const emailAddress = "team@echobind.com"; type emailDomain = typeof emailAddress extends `${string}@${infer domain}.${string}` ? domain : never; // type emailDomain = "echobind" ``` Remember, any of these types only work in development with literal types. When the program is actually run, all of these types are removed from the code, so this doesn’t replace runtime code. Instead, these tools can help you adjust and transform your types to provide better type safety and editor auto-completion without writing a bunch of extra code. --- [View on echobind.com](https://echobind.com/post/slicing-typescript-literal-strings) --- # Getting Started with Next.js, GraphQL and React Query (1/3) > Curious about Next.js, GraphQL, or React Query but not sure where to start? Follow along with Preston for a three-part series on setting up and using all three. _By Preston Kelly · 2022-09-28_ ## TL;DR Add a GraphQL endpoint, and wrap your app with `QueryClientProvider` from [@tanstack/react-query](https://tanstack.com/query/v4). Next, create an async function and use `GraphQLClient` from [graphql-request](https://github.com/prisma-labs/graphql-request) to request data. ## Create Next App Start by creating a basic [Next.js](https://nextjs.org/) app with the following command, `npx create-next-app@latest --ts` . This command will create a new Next.js app with a TypeScript configuration, hence the `--ts` flag. Head to the `/pages` directory and you'll notice an `index.tsx` page and an `_app.tsx` page. You'll see an `/api` directory. This is where we'll add our GraphQL server. Let’s transition to configuring the server. Create a new file in the `/api` directory and name it `graphql.ts`. This file will serve as the single endpoint we need for GraphQL. To import GraphQL we'll need 3 packages. Install these packages with this command `npm install apollo-server-micro micro graphql`. Inside our new file, we are going to create our GraphQL [types](https://www.apollographql.com/docs/apollo-server/schema/schema) and [resolvers](https://www.apollographql.com/docs/apollo-server/data/resolvers/). In a typical app, you’d want to create a new file for these and import them elsewhere in the app. For this app, we'll create our types and resolvers in the same file. Let’s create a User type that has an id, firstName and lastName field. We’ll also add a Query type, which we will need when we create our resolver. ```typescript // pages/api/graphql.ts const typeDefs = gql` type User { id: ID firstName: String lastName: String } type Query { getUser: User } `; ``` The next step is to create the resolvers or in other words, tell GraphQL where to look for the data. In most cases, the resolvers will connect to your database or an external API source. In our case, we’ll return some hard-coded data. I’ll add an id of 1, a firstName of “Hello” and a lastName of “World”. Now that we have our types and resolvers set up, the next step is to pass these to `ApolloServer`. ```typescript // pages/api/graphql.ts const resolvers = { Query: { getUser: () => ({ id: "1", firstName: "Hello", lastName: "World" }), }, }; const apolloServer = new ApolloServer({ typeDefs, resolvers, }); ``` The final step is to create our handler function that will become our Next.js API route. Inside this handler function, we have to do two things. The first is to start the server. We then have to use `apolloServer.createHandler` to connect Apollo Server to Next.js. We’ll also disable bodyParser since this is handled by default in GraphQL. ```typescript // pages/api/graphql.ts import { gql, ApolloServer } from "apollo-server-micro"; import { NextApiRequest, NextApiResponse, PageConfig } from "next"; const typeDefs = gql` type User { id: ID firstName: String lastName: String } type Query { getUser: User } `; const resolvers = { Query: { getUser: () => ({ id: "1", firstName: "Hello", lastName: "World" }), }, }; const apolloServer = new ApolloServer({ typeDefs, resolvers, }); const startServer = apolloServer.start(); export default async function handler( req: NextApiRequest, res: NextApiResponse ) { await startServer; await apolloServer.createHandler({ path: "/api/graphql", })(req, res); } export const config: PageConfig = { api: { bodyParser: false, }, }; ``` ## React Query Let’s switch to the front end so that we can add [React Query](https://tanstack.com/query/v4) to our app. To begin, we'll need some packages for React Query. Install the packages we need with this command `npm install graphql-request @tanstack/react-query`. Head over to `/pages/_app.tsx` and create a `QueryClient` imported from `@tanstack/react-query`. We’ll wrap our app with `QueryClientProvider`, passing our `QueryClient` as the client parameter. This will allow us to access React Query throughout our entire app. ```typescript // pages/_app.tsx import "../styles/globals.css"; import type { AppProps } from "next/app"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; // Create a client const queryClient = new QueryClient(); function MyApp({ Component, pageProps }: AppProps) { return ( ); } export default MyApp; ``` We can now create a function that will allow us to query for our user on the back end. We’ll use `GraphQLClient` and pass in the GraphQL endpoint that we made. We can then use this to make requests. To query for our user, we need to add a `gql` statement and pass it as a parameter to a `graphQLClient` request. We’ll then use this function by passing it to React Query’s `useQuery` hook, along with a key `get-user`. From this hook, I’ve destructured the isLoading state and the data. I’ve added an if statement to check if the data is loading. All we have to do now is display the data using some styling from [Tailwind](https://tailwindcss.com/) and a couple of `p` tags. ```typescript // pages/index.tsx import type { NextPage } from "next"; import { useQuery } from "@tanstack/react-query"; import { GraphQLClient, gql } from "graphql-request"; const UserQuery = gql` query getUser { getUser { id firstName lastName } } `; const graphQLClient = new GraphQLClient("http://localhost:3000/api/graphql"); const fetchUser = async () => { return await graphQLClient.request(UserQuery); }; const Home: NextPage = () => { const { isLoading, data } = useQuery(["get-user"], fetchUser); if (isLoading) return

Loading...

; return ( <>

{data.getUser.id}

{data.getUser.firstName}

{data.getUser.lastName}

); }; export default Home; ``` ## Conclusion As you can see, adding GraphQL to a Next.js app takes only a few steps. Today, we created a Next.js app with a TypeScript configuration. We created a GraphQL server with types and resolvers, and added React Query to query for our hard-coded data. I encourage you to extend this project. Create a new type, create a resolver for that type, and add a new function to query for that data using React Query. ## Resources [https://tailwindcss.com/](https://tailwindcss.com/) [https://tanstack.com/query/v4](https://tanstack.com/query/v4) [https://nextjs.org/docs/basic-features/typescript](https://nextjs.org/docs/basic-features/typescript) [https://www.apollographql.com/docs/apollo-server/schema/schema](https://www.apollographql.com/docs/apollo-server/schema/schema) [https://www.apollographql.com/docs/apollo-server/data/resolvers/](https://www.apollographql.com/docs/apollo-server/data/resolvers/) [https://www.apollographql.com/docs/apollo-server/api/apollo-server/](https://www.apollographql.com/docs/apollo-server/api/apollo-server/) --- [View on echobind.com](https://echobind.com/post/getting-started-with-nextjs-graphl-and-react-query) --- # Design… It’s like Building a House > Good design goes far beyond aesthetics and is one of the most important things you can invest in while building a product. _By Lexi Namer · 2022-09-27_ A 5 Minute Video Explainer
### What is Design A product can be beautiful but if it doesn’t function well, it won’t be a good experience. Design goes beyond aesthetics and is a complex form of communication and problem solving. It is one of the most important things you can invest in while building a product to increase sales and revenue, foster brand and customer loyalty, and reduce development cost and time. Did you know that first impressions are 94% design related[^1] and 88% of online shoppers[^2] say they wouldn’t return to a website after having a bad user experience? Google now ranks websites based on UX, so having a great experience means you have an edge over your competitors. To understand some of what goes into the design process at Echobind and why investing in design is a smart business move, let’s consider how a house gets built. To build a beautiful and quality house, each of these aspects are critical, inform each other, and cannot be done in isolation. Design is the same and helps ensure you have a product customers love. ### Laying the Ground Work You wouldn’t buy the first plot of land you saw without doing your homework, getting a survey, exploring the neighborhood, understanding the zoning laws, and talking to friends and family, and the same goes for product development. Without doing discovery and research, you are making a big investment in product development based on assumption. Designers conduct market research, look at competitors, and interview you and your customers to validate all ideas, ensuring that the product you build is exactly what needs to be built to meet your business and customer needs. ### Building the Structure Next comes the structure. You wouldn’t start wiring the electric or picking out paint colors without ensuring the architecture is approved and the structures are sound. As designers, we rely on UX or user experience design to ensure that every step of your product is cohesive and has been intentionally laid out from the first moment someone lands on your product to when they leave. 83% of customers[^3] say a seamless experience across all devices is important, and we rely on responsive design techniques and industry design standards to ensure your product works just as well on a phone as it does on a desktop. ### Designing the Interiors Only after research has been done and the structure is in place do we focus on the UI or the user interface. Just like paint colors and the interior design of house can make someone feel confident and comfortable, interface design does the same. It is a strategic form of visual communication that leaves your customer with an elegant, cohesive, and delightful experience across all devices that reflects your brand and mission. We focus on the smallest of details across layout, typography, color usage, interactions, and design patterns to ensure that your product is just as beautiful as it is functional. ### Approving and Maintenance Once the house is built, it requires exhaustive approvals and tests to ensure that it is up to code and is ready for you to call home. In design, we use usability testing which catches up to 85% of errors before your product goes development or market. And once you are all moved in, a house requires regular maintenance and support to stay up to date. As trends evolve, customer needs shift, and your business changes, your product needs to be regularly maintained. Just as you would regularly test your air conditioner, designers use testing, audits, and other, and accessibility audits to ensure it is ADA compliant. ### The Swiss Army Knife of Product Development As UI and UX designers, we are not just painters, but researchers, strategists, architects, interior designers, handymen and so much more. Throughout the design process, we ensure that each and every step of your product is a modern and seamless experience across all devices, ensuring that every customer has a great experience. Additionally, by investing in design at the start of your project, it can save a tremendous amount of time and money over the course of the product development. Fixing a problem[^4] in development costs up to 10 times more than fixing it in design, and up to 100 times more if the product is already in market. At Echobind, we offer a full array of software design services and can help you discover the right solution, design a new product, improve and existing product, or even augment your internal team. Our process is iterative and flexible based on your needs, timeline, and budget, and our team is ready to help. [^1]: https://www.researchgate.net/publication/221516871_Trust_and_mistrust_of_online_health_sites [^2]: https://www.toptal.com/designers/ux/ux-statistics-insights-infographic [^3]: https://www.fullstory.com/blog/digital-experience-consumer-survey/ [^4]: https://s3.amazonaws.com/coach-courses-us/public/theuxschool/uploads/The_Trillion_Dollar_UX_Problem.pdf --- [View on echobind.com](https://echobind.com/post/what-is-design) --- # Using the currentColor CSS Property with SVG > The CSS currentColor property is a powerful tool for passing colors down the cascade, including to SVG elements. _By Alex Anderson · 2022-09-26_ The `currentColor` property provides the current text color of the selected element. The CSS cascade makes it so the child’s text color matches the parents, but the `currentColor` property can be used for any other color property as well, like background or border colors. ```css .parent { color: red; } .child { border-color: currentColor; /* Also red */ } ``` This also includes properties on [SVG](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics) elements on the page. Using `currentColor` as the `fill` or `stroke` color on a shape or path will have that element take on the text color of its parent. It even works with a CSS animation. It’s important to remember that this only works if the SVG is rendered directly on the page - it won’t work if the SVG is loaded with an `` tag. --- [View on echobind.com](https://echobind.com/post/currentcolor-css-property-with-svg) --- # Make Prisma Ignore a Migration Change > How to avoid resetting your database when you need to change a Prisma migration after it has been run. _By Cully Larson · 2022-09-23_ Have you ever made a change to a Prisma migration and received a warning like this when you run `prisma migrate`: ```text The migration `20220805133548_my_migration` was modified after it was applied. ? We need to reset the PostgreSQL database "mydb" at "example.com:3033". Do you want to continue? All data will be lost. ``` This warning appears because Prisma tracks changes to migrations. If a migration has been applied and is later modified, Prisma will know it and assume the database is out of sync with your migration. Since it can’t know exactly what was changed and only selectively apply those changes, it opts for re-applying all of the migrations. This is almost always what you want to happen. Changing a migration will likely cause your database to not match what is in your migrations. You probably should re-apply all your migrations at that point. However, sometimes you want to make a change to a migration file that wouldn’t cause a change to your database. For example: 1. Adding a comment to a migration. The migration still makes the same changes to the database it did before. But the file has changed, so Prisma will notice it. 2. Adding something like an `IF NOT EXISTS` statement to a query. This may not change the functionality of the query. It will still e.g. `CREATE` something like it did before. It just won’t `CREATE` it if it already exists. 3. A third reason. You know what it is (it’s whatever you have in mind that isn’t one of the reasons above). It should be noted that what is to follow is almost certainly a bad idea to do in production. It’s likely only going to be reasonable to do as part of your dev process. For example, if you’re working on a PR, you apply a migration, realize you want to add a comment to the migration, and then you don’t want to re-apply the migration and lose some test data you’ve set up. ## Tell Prisma to ignore the changes So we need to somehow tell Prisma that this change is fine and it should be ignored. We can do that by creating a new checksum for the migration file and then overwriting the checksum for that migration in the database (again, not a great idea in production or if you care about your database). ## Generate a new checksum You can create a new checksum for the migration file using this command: ```bash shasum -a 256 prisma/migrations/20220510001642_my_migration/migration.sql ``` This will produce a value like: ```text fc27e97b9a61877f7f59d59a69c8c0bd2cd3271bc44b9f208800ed458d18a10b prisma/migrations/20220510001642_my_migration/migration.sql ``` You’ll want the first value, the random looking string: `fc27e97b9a61877f7f59d59a69c8c0bd2cd3271bc44b9f208800ed458d18a10b` ## Update the database Now we need to update the database with our new checksum. Use your favorite database client and look at the `_prisma_migrations` table. This might be hidden in some database clients, so you’ll need to mess around with settings until it shows up. Prisma uses this table to track data about migrations. We’ll be looking at the `migration_name` and `checksum` columns. Find the entry in the database for your migration: ```sql SELECT "id", "migration_name", "checksum" FROM "_prisma_migrations" WHERE "migration_name" = '20220510001642_my_migration' ``` It will show you something like: ```sql id: f8b25a66-0020-4a32-9a56-7441665d9eda migration_name: 20220510001642_my_migration checksum: 6c2def14f22254ead278f805a5e63641319d546d2739de65da5fd98f615ba56b ``` Now update the `checksum` to the new value we generated: ```sql UPDATE "_prisma_migrations" SET "checksum" = 'fc27e97b9a61877f7f59d59a69c8c0bd2cd3271bc44b9f208800ed458d18a10b' WHERE "migration_name" = '20220510001642_my_migration' ``` ## Conclusion That’s it! Prisma now thinks it has applied your migration because the checksum in the database matches the current checksum of the migration file. You should be able to run `prisma migrate` again without any issues. Remember that this will not actually apply your migration again. As far as Prisma is concerned, it has already been applied. --- [View on echobind.com](https://echobind.com/post/make-prisma-ignore-a-migration-change) --- # Dynamic CMS-driven Redirects with Next.js > Redirects send a request for one page to another page, and are crucial for maintaining good SEO. We can manage these with our own Next.js app and any CMS. _By Alex Anderson · 2022-09-22_ Despite our best efforts, websites change all the time. Content from multiple pages might be combined into one, or maybe the title of a page has changed, causing the URL to change with it. But what happens to the links to these pages on other websites? Redirects are the solution built right into the foundations of the web. [An HTTP redirect](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections) simply takes an incoming request to the _source_ URL and tells the web browser to request the _destination_ page instead. Redirects are essential for maintaining search engine optimization and making sure that outside visitors coming to your website aren’t greeted with a 404 page. The trick is how to tell your server what sources to go what destinations and making sure these run on every request. ### Storing Redirects We could just have a static list of redirects in code, but that makes it so only developers can update those redirects. Often it’s content creators or marketers without programming experience who make these changes. Instead, we’ll store our redirects in a content management system. There are literally _dozens_ of CMS tools out there, including Wordpress, Sanity.io, StoryBlok and Contentful. The key feature you need is the ability to modify the schema to support custom collections and fields. For today, I’ll be using [Directus](https://directus.io). First, we’ll create a collection called `Redirects`. We only need three columns: `source`, `destination`, and `permanent`. ![](https://cms.echobind.com/assets/5c7a262a-bdaf-47a8-b269-925a9b02a3b8.png) Then we can use the CMS to add whatever redirect records we may need. Finally, we need to query our CMS to get all of the redirect records. Directus automatically generates a REST and GraphQL API for any collections you create. Here’s the GraphQL query that we’ll use to get our redirects. ```graphql query Redirects { Redirects { source destination permanent } } ``` Your CMS might be different, but all of them have some way to get the data out, whether it be an HTTP API or an SDK of some kind. I’ll leave it as an exercise to the reader to figure out how that works in your particular circumstances. > For some CMSs, you might need to adjust the permissions or use an API token to access the data in your Redirects collection. I chose to make the Redirects collection publicly accessible, but you might want to restrict it. Now that our redirects are stored and we have a way to get them, we need to teach our server how to apply them to requests. ### Serving Redirects If we were using a traditional server-side framework like Express, Fastify, or Koa, we could run middleware on every request that checks the URL and sends the redirect response if needed. ```javascript const redirects = []; function redirectMiddleware(req, res, next) { const pathname = new URL(req.url).pathname; const redirect = redirects.find(item => item.source === pathname) if (redirect) { const statusCode = redirect.permanent ? 308 : 307 return res .set("Location", redirect.destination) .status(statusCode) .send() } return next(); } // ... app.use(redirectMiddleware) ``` > It is possible to further optimize this code for performance and capability, perhaps by transforming our list into a map keyed by the source, but for our purposes this works just fine. That’s great for a dedicated server, but lots of apps these days are built with Next.js. While it does have support for [being embedded in a custom server](https://nextjs.org/docs/advanced-features/custom-server), Next.js is much happier being served from serverless functions. How can we ensure our redirects are checked on every page? ### Next.js Middleware Redirects Middleware has been stable since Next.js 12.2. This allows you to write a lightweight function which is run before every request. If you deploy your site on a fancy hosting platform like Vercel or Netlify, they’ll even run your middleware _on the edge_, which means close to your users. This makes middleware an excellent choice for running fast, simple checks as users browse your website. This example comes straight from the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware). It shows how you can hard-code redirects, or even use in-code logic, to redirect users. ```graphql // middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' // /about-2 is the destination export function middleware(request: NextRequest) { return NextResponse.redirect(new URL('/about-2', request.url)) } // /about/:path* is the source export const config = { matcher: '/about/:path*', } ``` Notice that `matcher` at the bottom? This is the `source` that we’ll be redirecting the user from. It tells Next.js to only run the middleware when the user visits a page that matches one of the matchers. You see, middleware might be fast, but it still adds time to the request. If it runs on every request it’s going to slow down every request. This is especially true if we’re making a request to our CMS every time a user visits a page. And we can’t dynamically set the matcher values — Next.js requires that those are static strings. No variables allowed. Middleware is still great when you need to redirect based on complicated logic involving headers, cookies, or other factors. But it’s going to be a bit too slow to use on every single page. Fortunately, Next.js has _another_ feature which has been around even longer than Middleware. It might not be as powerful as Middleware, but it’ll work great for our purposes. ### [Next.js Redirects](https://nextjs.org/docs/api-reference/next.config.js/redirects) Static redirects have been around since Next.js 9.5 and can be configured in `next.config.js` using an async function. That function returns a list of objects with `source`, `destination`, and `permanent` — exactly what we’re storing in our CMS. When the Next.js app is compiled, it will call the `redirects` function to create a static list of redirects. Then, for every request, it will quickly check the request URL against the redirect list and send the user to the right page. This _does_ mean we’ll need to rebuild our app any time the redirects list changes, but that likely won’t happen often, and most CMSs allow you to automatically call a webhook whenever a record is added or changed. We can call a URL provided by our hosting platform to trigger a new build. Here’s what that looks like using Directus Flows. ![](https://cms.echobind.com/assets/d9f90e7a-a605-4776-8335-6423daeae308.png) And what about the code? The way we set up our collection means the data is already in the shape Next.js expects. We just make the request and put it in the list of redirects. ```javascript // next.config.js const { default: request, gql } = require("graphql-request"); async function redirects() { let cmsRedirects = []; try { const results = await request( `${process.env.NEXT_PUBLIC_DIRECTUS_URL}/graphql`, gql` query Redirects { Redirects { source destination permanent } } ` ); cmsRedirects = results.Redirects; } catch (err) { console.error("Error getting CMS redirects:", err.message); } return cmsRedirects; } module.exports = { redirects, // ... more config here } ``` Add a redirect to the collection, deploy this to your hosting platform, and you should see any requests to the `source` bring you to the `destination` page. Excellent! --- [View on echobind.com](https://echobind.com/post/dynamic-cms-driven-redirects) --- # Ten Tips for Effective (and Less Frequent) Meetings > Here are ten ways Echobind strives to control the number of meetings on our team members' calendars while making the essential virtual meeting as productive, meaningful, and engaging as possible. _By Claire Surma · 2022-09-21_ Echobind is a fully remote software company with employees from all departments—creative, strategy, development, and partnerships—scattered across 15 states. Our remote culture relies on virtual communication as our primary tool for internal collaboration and strategic planning, as well as client-facing exchanges. Not surprisingly, although unfortunate, a Harvard Business Review survey recently noted that [71% of employees regard meetings as unproductive and inefficient](https://hbr.org/2017/07/stop-the-meeting-madness), and over half of those surveyed found unproductive meetings kept them from their meaningful work. Even more unfortunate for us, research suggests that[ the remote meeting consistently ranks at the top for being poorly run, not engaging, and ineffective](https://www.stevenrogelberg.com/virtual-meetings). Gulp! Here are ten ways Echobind strives to control the number of meetings on our team members' calendars while making the essential virtual meeting as productive, meaningful, and engaging as possible. 1. **Create an agenda for every meeting and make it visible to all meeting invitees beforehand.** The process of creating an agenda will help the facilitator ensure the intention is clear and weighty enough to warrant a meeting. Once an agenda is drafted, share it with all invitees and seek input from key stakeholders on additional topics that should be included. It is best to save meeting agendas in a place that is easy for attendees to access. 2. **Highlight anticipated actions or questions needing answers on the shared meeting agenda**. While there is a time and place for morale-boost gatherings (watercooler chats, occasional office celebrations, etc.) and the information meetings (company updates, quarterly reports, etc.), most meetings are action-focused. Aligning on the collective goals or outcomes of a meeting beforehand will help attendees stay on topic during the meeting and make recapping the accomplishments easier. 3. **Consider an email with bulleted action items instead of a meeting** if there isn't enough robust content to develop a meaningful meeting agenda. 4. **Contemplate the cost of the meeting and who needs to be in attendance.** Don't over-invite and utilize "required" and "optional" meeting invites. More attendees on a virtual call tend to lessen productivity. Remember, virtual meetings can always be recorded and shared with those whose presence is non-essential. Optional invites require a culture of trust in a colleague's ability to control workday productivity. To go back to point one, sharing meeting agendas beforehand will help optional invitees discern if their presence is needed. 5. **Leverage video recordings in place of live** **meetings.** This is especially helpful when a discussion is unnecessary or when sharing demos or product how-tos that may benefit from repeat watching. Echobind uses Loom videos as a substitute for non-essential internal and external meetings. Loom is an excellent tool because of its analytics to review who watched, and the call-to-action button features. 6. **Be a conscientious meeting facilitator and prime attendees for action.** Steven G. Rogelberg, researcher and author of[ The Surprising Science of Meetings: How You Can Lead Your Team to Peak Performance](https://www.stevenrogelberg.com/the-surprising-science-of-meetings-by-steven-rogelberg), gives the following tips on leading effective meetings: welcome folks as they enter (login), express gratitude for attendance, initiate meeting with a clear objective, and recognize accomplishments. 7. **Keep videos on during meetings whenever** **possible** to encourage the facilitator and attendees to minimize outside distractions and avoid multitasking. Video also gives visual cues to the facilitator on engagement. 8. **Wrap up meetings with intention.** Be mindful of the established end time; with five minutes left, revisit the anticipated actions on the meeting agenda and recap accomplishments. As a group, identify all decisions made, clearly state the individuals responsible for follow-up actions, and agree on the due dates of those actions. 9. **Adopt a company-wide communication app to exchange quick questions and needs to help avoid non-essential meetings**. At Echobind, we use Discord internally and lean into direct messages and public topic board features. We also utilize Slack for our external and client-facing communication. 10. **Promote a company-wide "heads down day"** free from meetings and also free from guilt when denying or rescheduling non-necessary outside meeting requests. We have seen the benefits when focused energy transforms meetings. Virtual meetings don't have to be a time-suck. When done with intention, meetings are a valuable tool for developing strategy, decision-making, and productivity. **Resources:** - [https://hbr.org/2017/07/stop-the-meeting-madness](https://hbr.org/2017/07/stop-the-meeting-madness) - [https://www.stevenrogelberg.com/virtual-meetings](https://www.stevenrogelberg.com/virtual-meetings) --- [View on echobind.com](https://echobind.com/post/ten-tips-for-more-effective-and-less-frequent-meetings) --- # Running Cypress tests in parallel in GitHub Actions > Making Cypress run in parallel can speed up your build times and help you have more confidence your app works correctly. _By Cully Larson · 2022-09-16_ End-to-end tests can take forever to run. And what are they really doing with their time? Clicking some buttons, typing some text, and engaging in a lot of waiting around. E2e tests have got to spend—I don’t know for sure, so I’ll make up a number—half their time just waiting for things to happen. If they’re essentially idle a lot of the time, it makes sense to run e2e tests in parallel. [Cypress](https://www.cypress.io/) supports [parallelization](https://docs.cypress.io/guides/guides/parallelization). However, and I’ll leave it up to the reader to decide why this is the case, Cypress doesn’t make it very clear that in order to run tests in parallel you need a Cypress Dashboard account. Cypress Dashboard is what coordinates processes and load balances tests. Cypress Dashboard has a free tier. That might work for some projects. It could be worth paying for a plan for enterprise projects. They have some features that are appealing beyond parallelization. But what if you don’t want to pay for a plan? There’s a project called [Sorry Cypress](https://sorry-cypress.dev/) that does something very similar to Cypress Dashboard. They have paid plans if you want them to host the dashboard. Or you can host your own for free. But, stick with me here, what if you don’t want to pay anything and you don’t want to host your own dashboard service? Read on, dear reader. ## Split up our jobs This is an article about running tests in GitHub Actions. So let's talk about jobs. By the end of the article, we’ll create a job that runs the e2e tests in parallel. Each of those jobs will likely need a production build of the project to run against. But rather than have each individual e2e job build the project, we’ll perform the project build in its own job and share the build with the e2e jobs. ```yaml name: CI on: [push] env: NODE_ENV: test DATABASE_URL: postgresql://postgres:postgres@localhost/myapp_test?schema=ci jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v2.1.2 with: node-version: 16.13.2 cache: yarn - name: Install packages run: yarn install --frozen-lockfile - name: ESLint run: yarn lint - name: Check TypeScript run: yarn typecheck # Build app - name: Build App run: yarn build # Save build for other jobs - name: Save build folder uses: actions/upload-artifact@v2 with: name: the-build if-no-files-found: error path: build retention-days: 1 ``` Now we can use this build in our e2e jobs. If you have other jobs besides e2e tests (e.g. unit tests, API tests, stress tests, etc) you can use the build in them as well. ## An end-to-end job As a baseline, let’s look at our e2e job without parallelization. ```yaml e2e: name: e2e Tests runs-on: ubuntu-latest needs: build services: postgres: image: postgres:13.2 ports: ['5432:5432'] # Make sure the database is ready before we use it options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 # These need to be set on the service or it won't start for some reason env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: myapp_test steps: - name: Checkout code uses: actions/checkout@v2 - name: Download the build uses: actions/download-artifact@v2 with: name: the-build path: build - name: Setup Node uses: actions/setup-node@v2.1.2 with: node-version: 16.13.2 cache: yarn - name: Install packages run: yarn install --frozen-lockfile - name: Run Cypress e2e tests uses: cypress-io/github-action@v4 with: # we have already installed all dependencies above install: false start: yarn test:server wait-on: 'http://localhost:3001' command: yarn cypress run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Cypress Screenshots uses: actions/upload-artifact@v1 # Only capture images on failure if: failure() with: name: cypress-screenshots path: cypress/screenshots - name: Upload Cypress Logs uses: actions/upload-artifact@v1 # Only capture logs on failure if: failure() with: name: cypress-logs path: cypress/logs - name: Upload Cypress Videos uses: actions/upload-artifact@v1 # Only capture videos on failure if: failure() with: name: cypress-videos path: cypress/videos ``` This will run all of our tests in one job. To begin our parallelization journey, we need to run multiple versions of this job. We can do that with a [matrix strategy](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs). ```yaml strategy: fail-fast: false matrix: containers: [0, 1, 2, 3, 4] ``` This will run our e2e tests in five different jobs/containers. However, as it is, each job will do the same thing; they will each run all of our tests. ## Split up our tests We need a way to split our tests up. We could use an orchestration service like Cypress Dashboard or Sorry Cypress, as discussed. But without something like that, we need a way to split our tests up [deterministically](https://en.wikipedia.org/wiki/Deterministic_algorithm) (the same way every time), so that each of our five e2e jobs knows which tests it should run, each of the tests is only run once, and none of the tests are left out (all of them are run). `cypress run` has a `--spec` argument that allows us to define which test files should be run. If we can split our test files up, we can pass them to `--spec`, and only those test files will be run. But how to split up our tests? The simplest approach is to get a list of all the test files, sort them in a consistent way, and then divide them evenly among each of our e2e jobs. We need a script for that. ```typescript // cypress-spec-split.ts import fs from 'fs/promises'; import globby from 'globby'; import minimatch from 'minimatch'; // These are the same properties that are set in cypress.config. // In practice, it's better to export these from another file, and // import them here and in cypress.config, so that both files use // the same values. const specPatterns = { specPattern: 'tests/e2e/**/*.cy.{ts,tsx,js,jsx}', excludeSpecPattern: ['tsconfig.json'], }; // used to roughly determine how many tests are in a file const testPattern = /(^|\s)(it|test)\(/g; const isCli = require.main?.filename === __filename; function getArgs() { const [totalRunnersStr, thisRunnerStr] = process.argv.splice(2); if (!totalRunnersStr || !thisRunnerStr) { throw new Error('Missing arguments'); } const totalRunners = totalRunnersStr ? Number(totalRunnersStr) : 0; const thisRunner = thisRunnerStr ? Number(thisRunnerStr) : 0; if (isNaN(totalRunners)) { throw new Error('Invalid total runners.'); } if (isNaN(thisRunner)) { throw new Error('Invalid runner.'); } return { totalRunners, thisRunner }; } async function getTestCount(filePath: string): Promise { const content = await fs.readFile(filePath, 'utf8'); return content.match(testPattern)?.length || 0; } // adapated from: // https://github.com/bahmutov/find-cypress-specs/blob/main/src/index.js async function getSpecFilePaths(): Promise { const options = specPatterns; const files = await globby(options.specPattern, { ignore: options.excludeSpecPattern, }); // go through the files again and eliminate files that match // the ignore patterns const ignorePatterns = [...(options.excludeSpecPattern || [])]; // a function which returns true if the file does NOT match // all of our ignored patterns const doesNotMatchAllIgnoredPatterns = (file: string) => { // using {dot: true} here so that folders with a '.' in them are matched // as regular characters without needing an '.' in the // using {matchBase: true} here so that patterns without a globstar ** // match against the basename of the file const MINIMATCH_OPTIONS = { dot: true, matchBase: true }; return ignorePatterns.every((pattern) => { return !minimatch(file, pattern, MINIMATCH_OPTIONS); }); }; const filtered = files.filter(doesNotMatchAllIgnoredPatterns); return filtered; } async function sortSpecFilesByTestCount(specPathsOriginal: string[]): Promise { const specPaths = [...specPathsOriginal]; const testPerSpec: Record = {}; for (const specPath of specPaths) { testPerSpec[specPath] = await getTestCount(specPath); } return ( Object.entries(testPerSpec) // Sort by the number of tests per spec file, so that we get a bit closer to // splitting up the files evenly between the runners. It won't be perfect, // but better than just splitting them randomly. And this will create a // consistent file list/ordering so that file division is deterministic. .sort((a, b) => b[1] - a[1]) .map((x) => x[0]) ); } export function splitSpecs(specs: string[], totalRunners: number, thisRunner: number): string[] { return specs.filter((_, index) => index % totalRunners === thisRunner); } (async () => { // only run this if called via the CLI if (!isCli) { return; } try { const specFilePaths = await sortSpecFilesByTestCount(await getSpecFilePaths()); if (!specFilePaths.length) { throw Error('No spec files found.'); } const { totalRunners, thisRunner } = getArgs(); const specsToRun = splitSpecs(specFilePaths, totalRunners, thisRunner); console.log(specsToRun.join(',')); } catch (err) { console.error(err); process.exit(1); } })(); ``` _Note that this script was roughly adapted from_ [_this one_](https://gist.github.com/hosuaby/dd5d0c52bc20558568ae17c5a6d8ceef)_._ Run the script like this: `yarn --silent ts-node --quiet cypress-spec-split.ts 5 2` where `5` is the total number of jobs, and `2` is the number of the current job (starting with `0`). The second parameter is needed so that we know which job to assign test files to (each job gets its own, unique set of tests). Essentially, this script: 1. Fetches a list of all the e2e test files. 2. Naively tries to figure out how many tests are in each file. 3. Sorts the list of tests by the number of tests in each file. 4. Assigns each file to a specific job (i.e. the number value of `thisRunner`). It attempts to evenly divide the tests between jobs. That’s why they’re sorted by the number of tests in each file, so that one job doesn’t end up with all the files with the most tests. Without this, some jobs could take much longer than others. Even so, it’s unlikely that the script will exactly evenly divide the test files. That’s one upside of an orchestration service (it will divide tests more evenly). But in practice, this works well enough. 5. Outputs the list of tests for a job so that it can be directly passed to `--spec`. To run our tests, we will update the e2e test step in the workflow and use our script: ```yaml - name: Run Cypress e2e tests uses: cypress-io/github-action@v4 with: # we have already installed all dependencies above install: false start: yarn test:server wait-on: 'http://localhost:3001' # NOTE: This doesn't work. Keep reading to find out why. command: yarn cypress run --spec $(yarn --silent ts-node --quiet cypress-spec-split.ts 5 ${{ matrix.containers }}) env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` The `5` is the total number of jobs and `${{ matrix.containers }}` is the number of the job from our matrix (i.e. one of `[0, 1, 2, 3, 4]`; which is why we start them from zero). However, we have a problem. Unfortunately, [cypress-io/github-action](https://github.com/cypress-io/github-action) doesn’t allow us to use [command substitution](https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html), environment variables, or [context values](https://docs.github.com/en/actions/learn-github-actions/contexts). We could just run the test command manually instead of using `cypress-io/github-action`: ```yaml - name: Run Cypress e2e tests run: > doppler run --preserve-env -- yarn start-server-and-test 'yarn test:server' http://localhost:3001 "yarn cypress run --spec $(yarn --silent ts-node --quiet scripts/cypress-spec-split.ts 3 ${{ matrix.containers }}) " ``` But that can end up being a bit complicated with waiting for test servers to start, etc. `cypress-io/github-action` does that for us, along with a lot of other things. And `cypress-io/github-action` will continue to improve. Using it is a better option in the long term. If we want to keep using `cypress-io/github-action`, we need to create another script that basically just runs `cypress run` and fills in the `--spec` argument. ```typescript // cypress-ci-run.ts /** * This script runs Cypress tests in CI. It exists because we need to get split * up the tests between multiple runners, but we can't run that script in a way * that will pass a value to --spec directly, using cypress-io/github-action's * `command` property. It just won't let us include an env variable or do * command substitution. * * So, we can either just not use cypress-io/github-action or use a script like * this one to run the tests. */ import { exec } from 'child_process'; type GetEnvOptions = { required?: boolean; }; function getEnvNumber(varName: string, { required = false }: GetEnvOptions = {}): number { if (required && process.env[varName] === undefined) { throw Error(`${varName} is not set.`); } const value = Number(process.env[varName]); if (isNaN(value)) { throw Error(`${varName} is not a number.`); } return value; } function getArgs() { return { totalRunners: getEnvNumber('TOTAL_RUNNERS', { required: true }), thisRunner: getEnvNumber('THIS_RUNNER', { required: true }), }; } (async () => { try { const { totalRunners, thisRunner } = getArgs(); const command = `yarn cypress run --spec "$(yarn --silent ts-node --quiet scripts/cypress-spec-split.ts ${totalRunners} ${thisRunner})"`; console.log(`Running: ${command}`); const commandProcess = exec(command); // pipe output because we want to see the results of the run if (commandProcess.stdout) { commandProcess.stdout.pipe(process.stdout); } if (commandProcess.stderr) { commandProcess.stderr.pipe(process.stderr); } commandProcess.on('exit', (code) => { process.exit(code || 0); }); } catch (err) { console.error(err); process.exit(1); } })(); ``` This script: 1. Pulls the total number of jobs (`totalRunners`) and the number of this job (`thisRunner`) from env variables. 2. Executes the `cypress run` command with `--spec`. ## Put it all together Taking all we’ve built up so far and with our scripts located in `scripts/cypress-spec-split.ts` and `scripts/cypress-ci-run.ts` (relative to the root of the project), we get this workflow: ```yaml name: CI on: [push] env: NODE_ENV: test DATABASE_URL: postgresql://postgres:postgres@localhost/myapp_test?schema=ci jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v2.1.2 with: node-version: 16.13.2 cache: yarn - name: Install packages run: yarn install --frozen-lockfile - name: ESLint run: yarn lint - name: Check TypeScript run: yarn typecheck # Build app - name: Build App run: yarn build # Save build for other jobs - name: Save build folder uses: actions/upload-artifact@v2 with: name: the-build if-no-files-found: error path: build retention-days: 1 e2e: name: e2e Tests runs-on: ubuntu-latest needs: build strategy: fail-fast: false matrix: # Run copies of the current job in parallel. These need to be a # continuous series of numbers, starting with `0`. If you change the # number of containers, change TOTAL_RUNNERS below. containers: [0, 1, 2, 3, 4] services: postgres: image: postgres:13.2 ports: ['5432:5432'] # Make sure the database is ready before we use it options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 # These need to be set on the service or it won't start for some reason env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: myapp_test steps: - name: Checkout code uses: actions/checkout@v2 - name: Download the build uses: actions/download-artifact@v2 with: name: the-build path: build - name: Setup Node uses: actions/setup-node@v2.1.2 with: node-version: 16.13.2 cache: yarn - name: Install packages run: yarn install --frozen-lockfile - name: Run Cypress e2e tests uses: cypress-io/github-action@v4 with: # we have already installed all dependencies above install: false # build: yarn build start: yarn test:server wait-on: 'http://localhost:3001' command: yarn ts-node scripts/cypress-ci-run.ts env: # the number of containers in the job matrix TOTAL_RUNNERS: 5 THIS_RUNNER: ${{ matrix.containers }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Cypress Screenshots uses: actions/upload-artifact@v1 # Only capture images on failure if: failure() with: name: cypress-screenshots path: cypress/screenshots - name: Upload Cypress Logs uses: actions/upload-artifact@v1 # Only capture logs on failure if: failure() with: name: cypress-logs path: cypress/logs - name: Upload Cypress Videos uses: actions/upload-artifact@v1 # Only capture videos on failure if: failure() with: name: cypress-videos path: cypress/videos ``` I tried to make this as general as possible, but it’s not going to exactly fit everyone’s needs. I’m hoping it gives you enough to work with if you want to adapt it to your specific use case. ## Results At Echobind we’ve been running this solution in a production app for a while. When we rolled it out, it dropped CI runtimes from 30-40 minutes down to about 20 minutes. The "billable time" was roughly the same as it was before; maybe a few minutes more due to the overhead of setting up each parallel job. We played around with the number of jobs. The benefit of adding more quickly diminishes. - Two jobs save about 5-15 minutes. - Three jobs save about 7-17 minutes. - Four jobs save about 9-19 minutes. - Five jobs save about the same as four. Run times for GitHub Actions are wildly inconsistent, so these values are general and not exact. We decided to go with three concurrent jobs. Going up to 4-5 jobs didn’t add enough benefit to make it worthwhile. But you can experiment and see what works for you. ## Rant Cypress should do this out-of-the-box. There’s no reason it can’t implement naive test splitting internally, just as we’ve done above. And it would do a better job of it. I mean, [Playwright](https://playwright.dev/docs/test-parallel) does it. Hopefully, Cypress will get on board someday and this article will become a useless artifact of progress. ## Conclusion Who reads conclusions? I’m just going to tell you what we did. You either read the article and did all the things and don’t need me to remind you, or you skimmed the article and got what you needed and skipped this part (that’s what I would have done). So no one is reading this except whoever is reviewing this article before publication—thanks for the review, btw! Anyway, what did we do? We split up our GitHub Actions workflow into multiple jobs, we spilt our tests up so that we can run them in parallel, and we set up a job matrix for our e2e tests so that multiple jobs run at the same time. ## What’s next? Well, you could adapt this to run your local e2e tests in parallel. That’s a bit tricky if they all share the same database. But keep your eyes on this space, because exactly that is coming soon! ## Resources - [https://docs.cypress.io/guides/guides/parallelization](https://docs.cypress.io/guides/guides/parallelization) - [https://sorry-cypress.dev/](https://sorry-cypress.dev/) - [https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs) --- [View on echobind.com](https://echobind.com/post/running-cypress-tests-in-parallel) --- # CSS Custom Properties for Stacked Transforms > The power that CSS Transforms provide is undeniable. Stacking CSS transforms lets you choose the order that the transforms are applied. _By Alex Anderson · 2022-09-14_ ### tl;dr - Use the transform properties, like `rotate` or `translate` to apply transformations. - Combine CSS custom properties for each transformation to apply stacked transforms from different selectors # Stacked Transforms The power that CSS Transforms provide is undeniable, but it comes at a cost. Anyone who’s ever tried to combine transformations using multiple selectors will tell you - it’s not as easy as you’d think. Take a look at this ```css .box { width: 50px; height: 50px; background-color: rebeccapurple; } .rotate { transform: rotate(45deg); } .translate { transform: translate(100px, 0px); } ``` Suppose you were to use them with this HTML element: `
`. What do you think the result would be? Since the `.translate` class is below the `.rotate` class in the CSS file, the styles override, and the `translate` property isn’t combined. The end result is a box that is translated 100px to the right. The rotation is completely ignored. Fortunately, new properties have been [added to most browsers](https://caniuse.com/mdn-css_properties_translate) which allows each transform to be applied separately. ```css .rotate { rotate: 45deg; } .translate { translate: 100px 0px; } ``` This neatly solves that problem, but it introduces another one. ### Applying Multiple Transformations It’s not obvious, but applying multiple transformations with the `transform` property actually _stacks_ the transformations. If you apply a `rotate` before a `translate`, the element will actually translate based on the rotation that was previously applied. The same thing goes for a `scale` translation - it multiplies any `translate` transforms by however much the element was scaled. The separate `rotate`, `scale`, and `transform` CSS properties apply their transforms atomically, without considering other transforms which might have been applied. In other words, the separate transform properties do _not_ stack. Sometimes stacking transforms is exactly the behavior you want, but you once again run into the problem of combining transforms from different selectors. To solve that, we need to be a little tricky. ### Custom Property Transforms Using CSS Custom Properties, we can create selectors which let us change individual transform properties without overriding the entire `transform`. We create a separate CSS Custom Property for each transform property we want to change, and then combine them all in a single `.transform` class. Here’s what that looks like. ```css .transform { --translate-x: 0; --translate-y: 0; --rotate: 0; --scale-x: 1; --scale-y: 1; transform: translateX(var(--translate-x)) translateY(var(--translate-y)) rotate(var(--rotate)) scaleX(var(--scale-x)) scaleY(var(--scale-y)); } .rotate-45 { --rotate: 45deg; } .translate-x-100 { --translate-x: 100px; } ``` Then we can apply our classes like this: `
`. > If this looks a lot like atomic CSS libraries,[ like Tailwind](https://tailwindcss.com), you’d be right! This is exactly the technique Tailwind uses to apply transforms. This doesn’t give us much flexibility for stacking our transforms, though. It only lets us apply translates, then rotations, then scales. Fortunately, creating separate classes that alter the order is trivial at this point. ```css .transform-rotate-first { transform: rotate(var(--rotate)) translateX(var(--translate-x)) translateY(var(--translate-y)) scaleX(var(--scale-x)) scaleY(var(--scale-y)); } ``` Adding that class to our element would make it rotate first, then translate the element using whatever angle it was rotated to. --- [View on echobind.com](https://echobind.com/post/css-custom-properties-for-stacked-transforms) --- # Finding flaky Cypress tests by simulating a slow network > Tracking down flakey tests is a challenge, but maybe slowing down your network can help track down the problem. _By Cully Larson · 2022-09-09_ [https://www.loom.com/share/baf4691603084259aa4d2d06e3c517cb](https://www.loom.com/share/baf4691603084259aa4d2d06e3c517cb) You have some flaky tests in Cypress: sometimes a test fails, sometimes it doesn’t, or different tests fail every time you run your test suite. They might be flaky because **some of your tests are ending while a network request is still pending** or **a request fires after Cypress thinks the test has ended**. Your test tears down, another test sets up, and then your request goes through and fails. Often you can locate these tests by **simulating a delay in your API responses**. This is especially helpful if the **tests are failing in** [**CI**](https://en.wikipedia.org/wiki/Continuous_integration) **only and not locally**. Simulating a delay essentially makes the problem more consistent. It doesn’t just crop up some of the time, when you lose the [race condition](https://en.wikipedia.org/wiki/Race_condition), it always happens. Here’s an example using GraphQL and Next.js: ```typescript // pages/api/graphql.ts const sleep = (delay: number) => { return new Promise((res) => { setTimeout(res, delay); }); }; export default async function handler(req: NextApolloHybridRequest, res: NextApiResponse) { await sleep(500); // simulate slow network return server.createHandler({ path: GRAPHQL_PATH })(req, res); } ``` And then run your tests again. You'll likely see errors in all the places where this issue happens. Tip: The culprit is probably the test _before_ the one that fails since it's the one that made the request. To resolve this, make sure your test confirms that the request went through (using `cy.intercept` and `cy.wait`, [examples here](https://www.kgajera.com/blog/2022/07/14/flaky-cypress-tests-due-to-unfinished-network-request/) and [here](https://docs.cypress.io/api/commands/wait#Alias)) or that it's looking for something on the page that will only appear when the request is complete (e.g. a success/error message). One caveat with looking for something on the page: your app might be doing optimistic updates (showing that something worked before the request is complete), so it might not be as reliable. Remember to remove this delay before committing your changes. --- [View on echobind.com](https://echobind.com/post/finding-flaky-cypress-tests) --- # Echobind Ships Expo TypeScript Template 2.0.0 > We are excited to announce we have shipped a new version of our Expo React Native TypeScript template. Here are the latest changes. _By Jenn Robison · 2022-08-23_ We are super excited to announce we have shipped a new version of our [Expo](http://expo.dev) [React Native](https://reactnative.dev/) [TypeScript](https://www.typescriptlang.org/) template. You may not even know we had a template out there, but our team has been busy using the first version on our mobile application projects, and we were ready to embrace some of the latest changes in the React Native & Expo communities. Not familiar with our template? Here’s the tl;dr (too long; didn’t read)… * An opinionated [Expo](https://docs.expo.dev/) Bare Workflow template that will allow you to start a new React Native project quickly with TypeScript, Prettier, ESLint, and some custom configurations to improve the development process. 💖 You might ask why Echobind has a template. Similar to our open source project, [Bison](https://github.com/echobind/bisonapp), we have implemented the same solutions many times for our clients, so why not make it an easy-to-use format and share it with the community? **What’s new?** This newest version of the Expo template upgrades from SDK version 44.0.2 to bring it current with the latest 46.0.2 (as of publishing this article). Check out [Expo’s blog](https://blog.expo.dev/expo-sdk-46-c2a1655f63f7) about the release of this version, as there are some really great improvements. This includes the upgrade from React 17 to 18, bringing in the latest and greatest of React Native and the new architecture of version 0.69.x. There are several dependency updates for StoryBook and React Native Navigation, among others. It’s super simple to give our template a try! Simply create a new Expo project and reference our template with the following command: ```bash expo init --template @echobind/expo-typescript ``` ***To learn more about Echobind's React Native capabilities, visit our [React Native capabilities page](/services/react-native-app-development).*** --- [View on echobind.com](https://echobind.com/post/echobind-ships-expo-typescript-template-2-0-0) --- # How to Blog in 10 Doable Steps > Aug 17, 2022 | If you’ve ever experienced staring at a blank screen waiting for the words to come, this guide will help you find success in publishing a truly exceptional blog post. _By Alex Anderson · 2022-08-17_ We’ve all been there. You know “content creation” is the trendy thing to do these days - you’ve gotta build that audience somehow! You figure the lowest-effort way to do that is writing a blog. The benefits are clear: * Blogs are a great way to share knowledge and ideas broadly. * Blogs are excellent for SEO and organic traffic. * Blogs don’t need the expensive equipment that video and podcast production does. * Blogs also don’t need consistency like a newsletter. Write whenever you feel like without sticking to a schedule. In short, a low barrier to entry. Perfect for the first-time creators. You buy a domain, spend way too long setting up the perfect website, and finally sit down to write your first post. As you write, you start to realize that maybe setting up the site was the easy part. You had a pretty good idea for the blog post, but putting it into words is more challenging than you thought. If you’ve ever experienced staring at a blank screen waiting for the words to come, this guide will help you find success publishing a truly exceptional blog post. As a software developer, I take large, complicated tasks and break them into smaller, more manageable tasks. For example, if I’m building an e-commerce checkout, my smaller tasks include wiring up the payment provider, creating a form for collecting contact information, writing a function to validate the shipping address, etc. We can use the same technique to break up writing a blog post into smaller steps. Let’s go through the steps and see how they work in practice. There’s value in considering all these steps, but feel free to adapt or omit any of them depending on how long your blog post is. A 300 word post likely won’t need a full-blown outline, but could benefit from finding a story. ## Pick a Topic For many reasons, this first step is the most important. Find a topic to write about - hopefully something that is both interesting to you and provides value to your readers. In other words, write about something that someone else would enjoy reading about. This isn’t as hard as it sounds - you writing about your topic means there is at least one person in the world interested in that topic, and it’s likely you aren’t alone in that. One topic-inspiring question is “What did I learn today?” It’s a good practice to keep a topic journal. You’ll often think of topics at random times throughout the day. Having a place to quickly note them will make it easier for you to find those ideas later. Obviously the topic guides what you write about, but it also informs how _long_ your blog post will be and how long it will take to write. There’s a spectrum of _breadth_ and _depth_ of topics. You could take a very narrow topic, like “How to wash your hands” and spend a few hundred words on it. But a much deeper treatment that goes into why hand washing is important, the history of hand washing, the effects of good vs poor hand washing, and several different hand-washing techniques could fill a whole book. Meanwhile, a broad but shallow “history of programming languages” topic that gives a survey without getting into specifics wouldn’t take very long to write. Unsurprisingly, going deep into such a broad topic could yield dozens of books. My advice: Start small. Keep your blog post under 500 words. Your goal should be to deliver as much value to the reader in as small a package as possible, a skill that develops with practice. If your topic is simply too broad or too deep to keep small, consider breaking it into smaller topics and creating a series of blog posts. Twitter has excellent examples of high value to word ratio. Just look for programming hot tips tweets or threads that break down complicated topics. Remember, the goal here is to publish, not just curate ideas. When you’re ready to write, pick the topic you’re most excited about. ## Educate, Inform, or Entertain? All media or “content” serves at least one of three purposes for viewers: Educate, Inform, or Entertain. Educational material is usually the broadest and deepest, providing more practical information about the topic. Think textbooks, video courses, or open-source project documentation. Media that informs might still be broad, but significantly less deep. Television news epitomizes this - there’s only so much airtime, so anchors hit the highlights and move on to the next story. This kind of blog post usually condenses a lot of knowledge and research into a more digestible package, just giving most interesting bits without sharing all the details. Email newsletters, press releases, many development tutorials, and “This week in X” blog posts demonstrate an information focus. Entertainment exists for amusement. This media tugs at our emotions and brain neurotransmitters to give us a satisfying feeling after we’ve consumed it. Not all entertainment is strictly “funny” or “comedic” - horror or action movies give us their own form of enjoyable thrill, but a blog post that lands a few good jokes is definitely entertaining to read. Guess which one of these readers enjoy the most? Yep, that’s right - entertainment. There’s a place for dry educational material or terse information, but the types of blog posts you likely want to write should always be entertaining. Otherwise, readers will lose interest and bail. You might notice that many of these overlap across different media. A Bob Ross painting show is primarily educational, but the way he dispenses life advice alongside paintbrush technique make the show delightful and entertaining. Think of educate, inform, and entertain as levers that you pull throughout your blog post. Make sure to keep a healthy helping of entertainment, usually by presenting the education or information in an interesting, enjoyable way. Avoid excessive formality - it can be off-putting and distracts from the actual value the blog post presents. One last time for emphasis: Entertain. Entertain. Entertain. If you want your reader to like what you’ve written, make sure it’s entertaining. ## Research & Brain Dump Now for the first bit of actual writing! This part is simple - take whatever you can think of about your topic, and get it out of your brain and onto paper - er - screen. If you don’t have enough knowledge in your brain about your topic, go find it somewhere, put it in your brain, and then write it down. Read other blog posts or books, watch videos, listen to podcasts, take courses, talk to experts or friends, experiment, and most of all _ponder_. Think about your topic and the angles you might want to take as you write about it. Of course, you might already know all that’s needed to write your blog post, and that’s great! Rush through this step and move on. The deeper and more broad your topic, the more time you’ll want to spend here. This is also a good time to do SEO analysis on the topic you are writing about. See if there are any searchable angles worth pursuing to get the best SEO possible on the blog post. Ahrefs is a good tool for this. ## Find the Story Humans _love_ storytelling. We can’t help it! Something about a beginning, middle, and end all tied together with a narrative is just so satisfying. Even better is when we _relate_ to the story being told. That makes narrative a _critical_ element in making entertaining media. I’m not saying you need to start every blog post with “Once upon a time…” or create some cheesy hypothetical situation (like I did). But you need to provide some element to explain why your blog post exists. Why should your reader care about what you’ve written? An easy trick: start with some problem, and promise a solution by the end of the blog post. The storyline could go through why the problem is a problem in the first place, possible solutions and why they fall short, and land on the actual solution and why it is superior. It’s a great way to hook interest, since any reader who has the same problem will be thrilled to eventually find the solution, and might find interest in the journey that gets them there. I find it helpful to write a “Narrative Statement” - a single sentence that encapsulates the entire message I want to deliver with my blog post. It serves as a guidepost for the rest of the writing process, something I can look at to know if I’m on the right track. ## Outline If the narrative statement is a single guidepost, an outline is a map showing the path our reader will follow as they read the blog post. Your outline consolidates your ideas into distinct sections, while still making it easy to move ideas around. Writing your outline can help you make sense of your brain dump. It’s a checklist to make sure you include everything that you want to talk about in your blog post. Treat your outline like a three act play, with a distinct beginning, middle, and end. Act 1 is all about introductions - what’s the setting, who are the characters, and what is the main conflict driving the plot? The introduction should tell the reader why they should care about the rest of the blog post, and whether they are the intended audience. This should happen fast with a “hook” in the first paragraph - something so compelling that the reader wants nothing more than to keep reading. Act 2 fleshes out the the conflict more - what is the problem we’re trying to solve, and how can we solve it? This is usually the largest chunk of the blog post. Act 3 ties up loose ends and puts a bow on the whole thing. The conclusion of a blog post often reiterates what happened in act 1 and 2, but that’s not required. They frequently include extra materials or references the reader can look at for deeper learning. Don’t overthink this, though. For smaller blog posts especially, a conclusion isn’t even necessary. Once you’ve got your three acts, use your brain dump to add bullet points underneath each act, fleshing out more specific details. Add as many levels of bullets as you want. I like to make bullets for each of the section headers, and then add a few details that I don’t want to miss. Don’t write too much here, just ideas. Save the writing for the next section. ## (Finally) Start Writing The time has finally come for you to take your reader on this journey. You have your narrative statement guidepost, your outline map, and hopefully a plan for keeping it entertaining while getting the message across. Start with the hook to catch attention and keep the reader interested. Share the narrative. Deliver the information. Educate. Whatever you need to do. Keep it conversational. Make every sentence make the reader want to read the next sentence. There are a [bajillion guides to better writing](https://www.google.com/search?q=tips+for+better+writing), so I'll keep this section brief. My one suggestion: If you want to get better at writing well, read good writing. Here is some of my favorite writing as examples: * [A Complete Guide to useEffect](https://overreacted.io/a-complete-guide-to-useeffect/) \- Deeply educational, but an excellent narrative keeps it interesting. * [Building a Magical 3D Button](https://www.joshwcomeau.com/animation/3d-button/) \- A masterclass in hook writing. * [The State of Being Stuck](https://mathwithbaddrawings.com/2017/09/20/the-state-of-being-stuck/amp/) \- An excellent anecdote, and the pictures add color to the whole piece. * [Not Your Problem](http://howtomakeanrpg.com/a/not-your-problem.html) \- The prose is dripping with voice - you can hear the intonation of each sentence as you read it. Also, be sure to finesse your title. Despite pithy sayings, _everyone_ judges a book by its cover and a blog post by its title. Your title is your one shot to get the reader to click. Say what you will about clickbait, but it works. My rule: It doesn’t count as clickbait if the blog behind the click delivers a lot of value. Finally, only say what you intended to say, and then be done. There’s no need to bore readers by drawing things out. ## Jazz It Up If you’re a really good writer, your words will speak for themselves. For the rest of us mere mortals, our blog posts likely need _something_ else to keep the reader engaged. It’s mostly about avoiding walls of text - anything to break things up visually to make the reading experience a little more enjoyable. Here are some suggestions: * For longer blog posts, split sections with headers. It makes the blog post easier to skim and adds a necessary visual separation between the different sections. * Take advantage of **formatting!** Underline, **bold**, _italicize,_ emoji _🥳,_ add color - whatever you need to get your point across. Formatting can even be used to tell helplessly dumb really funny jokes. * Add quotes and asides. If your research found a relevant quote from an expert, throw it in there! If you’ve got extra thoughts that don’t fit with the overall flow or topic but are still interesting, add them in too. See if you can format these to separate them from the rest of the blog post appearance. * Throw in some visual aids. Pictures, diagrams, graphics, comics. The stick figures in _The State of Being Stuck_ probably took minutes to make, but add an extra dimension of value to the post. [Excalidraw](https://excalidraw.com) is an excellent low-effort tool for creating diagrams. * If you’re writing about something code-related, include a formatted code block or an embedded CodePen or CodeSandbox. Bonus points if the embed is interactive. ## Draft & Revise There’s nothing wrong with publishing your first draft - I do it all the time. But I’m _100% confident_ that your _second draft_ will be mounds better than your first. At a minimum, you’ll catch any grammar and spelling mistakes that might have accidentally crept in. It’s also possible that you’ll think of other, better ways to present the information in your blog post. The key thing here is time. It’s difficult to be objective about your writing if you dive right into the second draft on the heels of your first. Instead, give it at least a day to settle. Sleep on it, so to speak, and give it a re-read the next day with a clear head. Also, one of the most powerful revision tools is reading aloud. Yes, aloud. Vocally. A conversational blog post should read aloud naturally. Sections that feel weird in your mouth as you say them are prime candidates for revision. Finally, try throwing your blog post into an editing tool like [Hemmingway](https://beta.hemingwayapp.com), which tells you how long and how readable your blog post is, and gives specific recommendations for how to improve it. Don’t feel like you have to follow every suggestion, but it can give you ideas for areas to look at. ## Get Reviews One of the hardest things about writing is getting out of your own head. You have ideas and notions for how things should be written, but all of us have blind spots. That’s why getting others involved is a crucial part of the writing process. Whether you have a friend who gives your final draft a once-over, or a trusted partner who collaborates with you every step of the way, a reviewer can either point out areas you can improve, or give you the confidence that your blog post is ready to publish. Remember, you don’t have to follow all of your reviewer’s suggestions. Consider all of them, but remember that ultimately you are the author. ## Publish & Share The time has come! There are likely a number of finishing touches to throw in - a cover image, SEO keywords, tags, and whatever else you need. But the main attraction is done. Throw it in your CMS of choice, smash “Publish”, and bask in your newfound celebrity! …but before you do that, do a quick double-check to make sure everything looks good. Often, when pasting into a CMS of some kind, the formatting gets off. A quick check of the published post pays off. Once the post is live, don’t forget to let people know about what you’ve written. To get the most eyeballs on it, do the full social media tour - Twitter, Facebook, LinkedIn, Reddit, Instagram, Hacker News, Slack, Discord - wherever there are people you think might be interested in your thing, post it. This is not the time for modesty - it’s time to showcase your work to the entire world. Then sit back and relax. Sometimes people enjoy watching the page view analytics while others don’t. I once had a blog post that had a huge spike in traffic. Come to find out someone had posted it on a social media site and a _ahem_ lively discussion was happening. (Another tip: maybe avoid reading the comments. It’s better for your mental health 😅). And with this blog post published you can focus your attention on other things. Like writing the next blog post! ## A Note on Procrastination Let’s take it back to the beginning. You’ve got an idea, you’ve got an inkling of how you want to write it, but you can’t bring yourself to start. This relates to the physical principle of _momentum_. Objects in motion tend to stay in motion, while objects at rest don’t spontaneously move. Motivation works the same way. Once you get started, you’ll be surprised by how far you go. It’s the starting that’s tough. [This blog post](https://www.deprocrastination.co/blog/3-tricks-to-start-working-despite-not-feeling-like-it) provides some ideas for doing just that. My recommendation: _write crap_. Write the worst possible words that you can, just to get _something_ on to the page. If you finish your writing session and all that you have is crap, that’s okay. At least you did something. You can try for better words next time. If even that is too difficult to do, you might need a bit of self-guided emotional therapy. Take a few moments and think about how you feel about writing your blog post. Sometimes, those feelings block us from recognizing the small, practical steps we can take towards progress. Merely _recognizing_ those feelings can help us break through the block and actually start working. **My advice to the young writer is likely to be unpalatable in an age of instant successes and meteoric falls. I tell the neophyte: Write a million words–the absolute best you can write, then throw it all away and bravely turn your back on what you have written. At that point, you’re ready to begin. \~ David Eddings** For me, writing is satisfying in its own right, even if I never publish. But I can see how others might detest the idea of writing. But like many things in life, improving and learning to enjoy an activity comes with practice. Hopefully you’ll find some of these suggestions and ideas helpful. The most important advice, though: practice. The more you write, the better you’ll get. Now if you’ll excuse me, I’ve got another blog post to write. --- [View on echobind.com](https://echobind.com/post/how-to-blog-in-10-doable-steps) --- # My Journey In Tech > Why and how I transitioned into Tech (UI/UX design) and what I’ve learned in 10 years. _By Alex Silcox · 2022-08-11_ I've been in the tech industry for almost 10 years. My path toward becoming an engineer was unconventional from the typical CS path. When I started my journey, I was a college student pursuing graphic design. Prior to that, I had been a henna artist and aspiring artist. My knowledge of coding had been limited growing up, mainly involving CSS to customize my Myspace profile (does that date me a bit?). So while in college, learning to code was an investment to be a better designer, wanting to understand the limitations and possibilities of web technology at the time. That investment quickly became a passion—I switched my major, spent my free time learning code, helping my peers, and took every opportunity to learn and build. I worked on personal, freelance projects and internships to build my portfolio. Within a couple of years, I found myself in my first full-time development role. Since then I've worked across multiple companies, from small agencies to mid-size corporations of various industries and projects. The last two years have been spent as a Senior Software Engineer at [Echobind](https://echobind.com/), a digital agency dedicated to providing bold, beautiful, and bulletproof web and mobile solutions for our clients. With this experience, I’ve learned some valuable lessons that not only helped me become a better engineer but also kept me interested in this industry, and put me on a career path I never expected years ago. Here are some of the things I’ve learned over time: ## There is No Linear Path To start, there’s no linear path toward growth. It’s not as much of a shock or stigma now, but back when I discovered coding as a career path, [boot camps had just started to take root](https://venturebeat.com/2015/11/08/coding-bootcamps-are-replacing-computer-science-degrees/) and I was fortunate to be one of the early adopters of online learning. This path enabled me to become a software engineer and showed me that success didn’t have to be achieved one way. Over time, I’ve only seen the field become more diverse, with many engineers with non-traditional backgrounds who are changing the development landscape. Like my own path, seeing the success of others has continued to shape my thinking that embracing unconventional can sometimes lead to the best opportunities toward growth and success, and we want to foster that the best we can. ## Stay curious and ask questions There is no amount of experience to negate the need to ask questions. In fact, asking questions is often the best channel for communication and learning, whether it’s how a feature or a piece of code works, or how another developer would approach a problem. Even as a senior engineer, I’m often curious about how others solve problems or implement code because there is always something new to learn or better approaches than my own, and a significant resource to learn is those around you. There are countless examples I’ve come across in my career, but one that comes to mind was when first learning about using object literal lookups instead of switch statements. This was all because of asking a coworker how they would approach returning from functions based on a value with many possibilities. It was a game changer for me and really reinforced how important asking others can be because the best ideas often come from others. There’s a time and place, but you’re only limiting yourself and your growth if you think you have to solve it all. ## Know your audience At some point in your career, you’ll realize the importance of communicating to different audiences—beyond the diagrams and tech jargon. At one of my previous jobs, I worked closely with our UX designer prototyping a sharing feature for a healthcare mobile app. Alongside them, we discussed the flow of authorization to allow a user to share their information with other users. At one point, they had a bewildered look on their face trying to understand what it would take to implement this, in both design and code. The UX lead stopped us and asked us to explain this in another way. Knowing they were lost, we restarted the conversation by finding the right way to help bridge the gap. One of the engineers in this scenario kept trying to bridge the gap with more tech jargon and traditional whiteboarding. But it was only causing more confusion. Realizing this person was a visual person and needed a relatable example, we switched gears. In this case, we found an analogy (ironically Starwars) that ended up working perfectly to explain the transactions that needed to take place for our sharing service to work. We cemented it further by sketching this transaction on the whiteboard. It may have not been a perfect example, but it was enough to get the point across. The point of the story is, that sometimes flowcharts or tech terms are not enough, and you will need the right soft skills to play to your audience. ## Code for your future self and others ![https://i.redd.it/hwqj7yx9vm211.jpg](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/bojl48tlgvgnoyqpnqxj.jpg) It’s bound to happen in your career (more often than you’d like) that you’ll run into code that is cryptic or overly complicated with no documentation of what it’s doing and the steps to get there. Often times code was written this way out of laziness, to appear intelligent or to try to solve it all with little attention to future-proofing. Without proper documentation, likely those who work on it will struggle to understand what or why it was implemented, or the learning curve will be too steep without the original author to explain. This will lead to headaches and complications. So do yourself and others a favor, and code for readability with: * Reusable code, but when it makes sense the most sense. Avoid overly generic functions that leave the question of what are we getting or expecting. **Each function or variable name should represent the result of the code or the value it holds**. * Break code down into digestible pieces. If your functions are handling multiple tasks, ask yourself if it’s possible to break it down so that it’s easier to understand. * Code with a natural flow in mind. If the code seems like magic or isn’t clear how things are interacting, it is an enemy, not a friend. * Leave the code in a better place. If you run into code that could be optimized, do so if you have the time. * Write comments when needed. While function/variable naming should be sufficient to describe what should be expected, comments will still help to describe the inner workings of a function. Documenting what the function does can provide better context for a named function or variable, especially if there is insider knowledge to some handlings within the function. * Write for diversity. Everyone has different learning styles, if you can try to approach your code and your PRs with this mindset, it will be beneficial to your team. ## Fighting burnout, knowing your limits, and learning to honor your boundaries. It’s possible that at some point in your career you’ll experience fatigue or [developer burnout](https://revelo.io/blog/software-engineer-remote-burnout). Coding is taxing on the brain, but there are so many external factors that can cause a breaking point if not mitigated or managed. I’ve suffered through bouts of burnout multiple times in my career during various projects, teams, etc. Even in my engineering role at Echobind, I’ve experienced burnout on client projects I’ve worked on as I’ve lost interest in the things that use to motivate me. As someone who has lived through seasons of anxiety and depression, I’ve become practiced to recognize and prioritize fighting burnout. It’s important to recognize when it happens, know and honor your limits, and proactively make changes or seek resources to help reduce or eliminate the burn. Some changes can include: * Prioritize physical and mental boundaries * Seek out things you do care about inside and outside work * Surround yourself with those who inspire and have your back * Be transparent about needs and seek help * Find opportunities to have fun, be creative, and give back * Prioritize rest, vacations, and even breaks While there is a very valid nuclear option (aka quitting your job), it’s best to analyze if there’s no other recourse before seeking this one out. Developer burnout is nothing to be ashamed of, and there are resources and communities out there to help you prevent, fight, and thrive. ## Lastly, pursue your purpose and find opportunities that will support it I’ll be honest, I have struggled with my path as an engineer over time. When I first started my career, I was very passionate about learning, building, and creating. It fulfilled many aspects of the things I craved and found fulfillment in. I have worked on many projects and with some fun technologies. Over time I found my motivations for being an engineer shifted, and I began to question if this was still the right path for me. Turns out it is, but I had to make a change to support the things I cared about—mentorship, community, and supporting others toward their success. It’s incredibly important to find and foster the things that really matter to you to continue in this (and any) industry, and be okay acknowledging when those things have changed. If you’re struggling to find fulfillment in your work, take the time to start asking yourself what really matters to you, what opportunities (whether at your company or community) are available to support them, and what’s holding you back from pursuing them. Sometimes this will lead to a direct shift in your career, sometimes it will lead to fostering it elsewhere. Regardless of how it directs you, you will need to be ready to embrace it when it happens because it may be the best thing you can do for yourself (and others). --- [View on echobind.com](https://echobind.com/post/my-journey-in-tech) --- # Bison Release 1.12.0 > We're excited to ship another version of Bison that not only improves the web apps we build for our clients, but also the developer experience of working on Bison apps. _By Kishan Gajera · 2022-08-09_ We're excited to ship another version of [Bison](https://github.com/echobind/bisonapp) that not only improves the web apps we build for our clients, but also the developer experience of working on Bison apps. Not familiar with Bison? It's [Echobind's](https://echobind.com/) starter repository for full-stack Next.js web apps. Check out the links below for more background on Bison and how to use it: * [Overview of Bison](https://vimeo.com/447643624) * [Creating Your Own JAMStack Blog in No Time with Next.js and Bison](https://blog.echobind.com/creating-your-own-jamstack-blog-in-no-time-with-next-js-and-bison-a3cdd507fd61) ## Upgrade to Next.js 12 and React 18 As always, there are so many improvements introduced with each version of [Next.js](https://nextjs.org/), and we’re excited to upgrade to the [latest version](https://nextjs.org/blog/next-12). In particular, we’ve been taking advantage of the new [middleware](https://nextjs.org/docs/advanced-features/middleware) feature to protect pages and API routes. This year, we are also looking into more ways to utilize React 18 features such as [server components](https://nextjs.org/docs/advanced-features/react-18/server-components) and [streaming rendering](https://nextjs.org/docs/advanced-features/react-18/streaming) to improve performance for client-side interactivity. In order to upgrade to React 18, this also required an upgrade to [Chakra](https://chakra-ui.com/) 2.0, so be sure to follow their [migration guide](https://chakra-ui.com/guides/migration) if you’re upgrading an older Bison app. ## Prisma Upgrading to the latest Prisma also provides many new features. One to highlight is how we are handling running seed scripts. We’ve introduced a new pattern for seeding development and production environments using two scripts, yarn db:seed and db:seed:prod respectively. Refer to our [documentation](https://github.com/echobind/bisonapp/blob/canary/packages/create-bison-app/template/prisma/SeedsAndScripts.md) to see how to organize your seed data and scripts. ## TypeScript We've upgraded Bison to use the latest version of TypeScript, [4.6.](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-6.html) In addition to that, we've also enabled the [strict compiler option](https://www.typescriptlang.org/tsconfig#noImplicitAny) and refactored the codebase to be even more strongly typed. This will help to enforce best practices to ensure all variables are typed which results in a more maintainable codebase. ### Path Aliases We’ve introduced the following path alias that will allow us to have cleaner imports by avoiding the use of relative imports: ```jsx "paths": { "@/*": ["./*"] }, ``` We’ve refactored all relative imports in the template to use this new alias, here’s one example: ```jsx // Using relative import import { prisma } from '../../lib/prisma'; // Using path alias import import { prisma } from '@/lib/prisma'; ``` ## ESLint Our [eslint-plugin-echobind](https://github.com/echobind/eslint-plugin-echobind) has been upgraded to use ESLint 8\. In addition to this, we also found and fixed a loophole in our CI that was allowing lint errors into the repository. ## Windows We’ve fixed a long-standing [issue](https://github.com/echobind/bisonapp/issues/127) that prevented creating a new Bison app on Windows. With many more improvements planned for our CLI, we’ve updated our CI to run our CLI tests on Windows to ensure all features work on Windows going forward. ## Contributing to the Bison Template Check out our [contributing guide](https://github.com/echobind/bisonapp/blob/canary/CONTRIBUTING.md) for more information on how you can help out. Whether it's reporting bugs, creating pull requests, or participating in discussions, all contributions are welcome! --- [View on echobind.com](https://echobind.com/post/bison-release-1-12-0) --- # Benefits of Implementing TypeScript Early > When you’re getting started on a new project, does it make sense to start off using TypeScript or to implement it later when it becomes more of a must-have? Let’s dive into when and why it’s better to start early. _By Mariah Grey · 2022-06-30_ When you’re getting started on a new project, does it make sense to start off using TypeScript or to implement it later when it becomes more of a must-have? Let’s dive into when and why it’s better to start early. ### **Find errors earlier, with fewer bugs** When you develop an application in JavaScript, your flow might look like this: 1. Make a change 2. Go to the app and check the changed part and run the affected tests. 3. Find out if anything broke. With TypeScript, you can make the change and immediately be alerted by a compiler error message or IDE real-time feedback to any type errors. Of course, the TypeScript compiler won't resolve every problem and won't warn you about all bugs, but it helps speed things up. The type information is also checked by the TypeScript compiler. The compiler will tell you when your code is not type-safe. It’s the classic benefit that type checking brings: _Catching bugs earlier in the development process_. You will catch and fix bugs during development instead of your customers finding them when your app is in production. The TypeScript compiler has a strict option that enables stronger type checking. With the strict option enabled, it can flag more potential problems. It’s best to not use the strict mode when migrating an existing JS project to use TypeScript. In a TypeScript migration project, starting with the strict checks disabled makes it possible for you to have the build passing and the program running with less effort. If you enable the strict checks, you’ll need to do more significant refactoring such as defining the types to make the compiler happy. It should be noted that this industry is using several techniques to catch bugs. Typescript and its static type checking are only one of these techniques. Some of the other recommended practices include Test Driven Development (TDD), pair programming, code reviews, design reviews, and linting. If you are using TDD and code reviews, you are likely already efficient in finding bugs early. In this case, adding TypeScript will not have as significant of an impact on improving quality. You should consider your situation and try to determine whether the benefits of TypeScript will outweigh the costs of implementation. ### Achieve b**etter syntax completion in IDEs** If you are using an IDE like VSCode, you will get better syntax completion with TypeScript. Maybe it doesn't sound like a huge developer experience bump, but each little improvement potentially saves you time. Another plus: when you can define the type or interface once and not have to constantly remember the structure, you can focus on writing better business logic. ### R**efactor more easily from JavaScript** Imagine you’ve joined a project and have the task of adding a new feature, but that feature is connected with legacy code. TypeScript will help by alerting you when you’ve made a change and need to update the same piece somewhere else within the file. Valid JavaScript code is _also_ valid TypeScript code, so you can migrate your codebase file by file. Usually, using strict mode in TypeScript is good practice but in this case, you’d start with false to prevent hundreds of compilation errors. Once all the files have been updated to use TypeScript, setting strict mode to true will give you a standard to stick with. Begin by setting a few configurations inside the `tsconfig.json` file: ```jsx `"strict": false,` `"allowJs": true, // will allow using .js files without checking the type "skipLibCheck": true // will skip checking types in used libraries` ``` With those options, you can migrate from JS to TS file by file, simply changing the extension from `.js(x)` to `.ts(x)`, adding types in the files, and fixing any errors that pop up along the way. ### Feel c**onfident about the codebase** JavaScript is weakly and dynamically typed, so when you initialize a variable with the value `let query = ''`, you may later do something by mistake, such as `query = true`, and it will be valid JS code. When using TypeScript, you can't change the type of the variable, so if you make the `let query = ''` variable, it will be `string` type and you won't be able to change its type by mistake. If you want to let a variable be more than one type, you always do it explicitly using union type, for example, `string | number`. TypeScript makes your code predictable and explicit. ### **Work as a team** JavaScript is adequate when working alone, but it’s sub-optimal for teams. It’s suitable for quick development. When working solo, It doesn’t matter if the code is hard to understand for others, as long as it makes sense to you. Most likely, you can understand it well enough to maintain it on your own. JavaScript allows for rapid development partly because it does not require any types or interfaces to be written. You are not “wasting” any time in writing this typing. I recently started working on a huge JavaScript codebase that had been authored by one developer over several years. My task was to implement a small enhancement, but I was unable to do it in a straightforward or easy-to-implement way because it was too hard for me to understand what was going on in the code. The original developer was able to still make changes to it, but for me, the code did not make sense. The coding style, or the lack of it, made it impossible for me to understand. Type declarations are part of what makes the code easier to understand. You can immediately see from the type declarations what data the function takes in. Interfaces describe the structure and typing for all complex parameters. There is no need to inspect the calling code, the function implementation, or the unit tests, to figure out what fields these complex objects are composed of. A shared understanding of the code becomes _vital_ when you are working as a team. Added type information brings better understanding. ### **Typing** As mentioned on its official website, **TypeScript is JavaScript with added Type annotation**. Now, the question may arise whether a statically typed language is better than a dynamically typed language. The answer is: it depends. Because of its type system, TypeScript has the following advantages over JavaScript: * It is easier to understand * It is faster to implement using modern IDEs * It is quicker to refactor * It gives better performance ### Language D**esign** There are many other typed JavaScript languages. What makes TypeScript unique is the language design. Anders Hejlsberg is a veteran programming language designer who has previously developed programming languages like Delphi, Turbo Pascal, and C#. He used those experiences to design TypeScript as a clean, elegant programming language. Most of the other programming languages first design their type system and then use it. But TypeScript designers first looked at the JavaScript use cases and later developed the type system so that it retrofits the JavaScript use cases. Also, unlike the C-family languages, which use nominal types, TypeScript uses structural types. Structural types are a way of relating types based solely on their members, unlike nominal types which say that two variables are type-compatible if and only if their declarations name the same type. TypeScript uses some of the most advanced type systems like [Union Types, Intersection Types, Differentiating Types, Nullable Types, Conditional Types, and Type Inference.](https://www.typescriptlang.org/) In terms of language design, it is comparable to other modern languages like Kotlin, Go, or Rust. ### D**evelopment Scaling** Due to its type system, elegant language design, and IDE support, TypeScript will lead to better development scaling compared to JavaScript in a large project. It is no wonder that the official slogan of TypeScript is: “**JavaScript that Scales.”** As development scaling is a significant factor in today’s software development industry, an increasingly large number of corporations and large open-source projects are using TypeScript. ### **Developers Love It** The TypeScript team at Microsoft has done an excellent job designing TypeScript as a pragmatic modern programming language. Their hard work is vindicated as the software development community loves TypeScript. A language can have excellent features, but it does not mean that the language will be popular. Also, some languages gain popularity (like Scala and CoffeeScript) but quickly fade away. Fortunately for TypeScript, it is already a trendy language. According to the GitHub Contributions, TypeScript has entered the Top 10 List (ranked 7th) and is one of the fastest-growing languages. It ranked fifth with a 161% increase in adoption last year. With almost 12 million downloads every week, it is one of the fastest adopted technologies within the JavaScript landscape. ### **Open Source** Microsoft first created TypeScript in 2014\. As Google was also planning to develop a similar typed JavaScript at that time, Microsoft and Google collaborated together to bring you TypeScript. It is also open-source with a very permissive Apache 2.0 license. Currently, TypeScript is supported by many other large corporations and software development communities. The following JavaScript frameworks have either implemented or plan to implement TypeScript: * Angular * Vue * Ember * Svelte With its enterprise-friendly features, which are very valuable in writing and maintaining a large code-base, more and more large companies are turning to TypeScript. ### **Superset Of JavaScript** Another killer feature of TypeScript is that it is typed JavaScript with some extra features which are not yet in JavaScript but may come in the future. When TypeScript first added Class and Module support, JavaScript did not have them. Once JavaScript incorporated them, TypeScript aligned itself with JavaScript. TypeScript looks at the ECMAScript **[specification](https://www.ecma-international.org/publications/standards/Ecma-262.htm),** and **[TC39 committee](https://github.com/tc39)** and implements Proposal, Draft, or Candidate features ahead of JavaScript. Most of the time, JavaScript ends up implementing the same features. For example, here are some TypeScript features that are not yet in JavaScript: * Optional Chaining * Nullish Coalescing * Enum Type * ECMAScript Private Fields * Top-Level await With a three-month release cycle, TypeScript can add proposed ECMAScript features much faster than official JavaScript does. ### When don’t you want to implement TypeScript? There are times you won’t want everything that TypeScript has to offer - for a simple landing page where JavaScript is only used for toggling class or another simple case, TypeScript isn’t worth implementing. Also, you have to remember that to take full advantage of TypeScript, you have to learn to use it on a sufficient level, and it can take some time. Other reasons include: * Quick prototypes are easy to develop with JavaScript * Single-use scripts (migration scripts etc.) are an excellent fit to be done with JavaScript JavaScript has the following advantages over TypeScript for its dynamic type: * No additional compiling step * Concise and succinct code * No need to learn the “extra” type system * Easier to write higher-level abstraction without a type system TypeScript does not come for free. There is some cost associated with using it: * It takes effort to write type annotations and maintain them * Type annotations add clutter to the code base ### Conclusion In the end, it comes down to your project and the amount of time and effort needed, and your team will have to weigh the pros and cons of implementation. If you decide to add TypeScript to your project, the benefits will appear quickly, from better code completion to bug prevention, and it will improve your teams’ lives when it comes to their code. --- [View on echobind.com](https://echobind.com/post/benefits-of-implementing-typescript-early) --- # Spring Peaks at Echobind > After a two-year hiatus from our Annual Summit tradition, we planned spring peaks across the country to bring our fully remote team together. _By Krystalyn Bauer · 2022-06-08_ Here at Echobind, we are a fully remote, nationally distributed team. We can be found in the four corners of the US and anywhere in between. One of our most beloved perks is our Annual Summit where we get our team together IRL to do something fun, somewhere awesome. After a two-year hiatus from our Annual Summit tradition, we wanted to make up for lost time but our Annual Summit isn't scheduled until later this year. Queue: Spring Peaks. Spring Peaks are our newly exercised regional meetups. Our first Spring Peak took place in sunny San Diego in the beautiful area of Encinitas. A short day of coworking, _office donuts required_, followed by tacos near the beach was the perfect start for many of us to meet each other for the first time in person. Our first Spring Peak was full of incredible views, great food, and lots of laughs. ![Team dinner](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/pwzil1cdamwgyvsbvgm7.jpg) ![Plate with steak, carrot, and potatoes](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/x5thugmsc6gsiap9ov4b.jpg) ![Sunset with palm trees](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/xcxx2h6i1t9qzb8hkv9e.jpg) ![Company photo](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/ztl6ytxozwajtwfvg2nw.jpg) For our next Spring Peak, we moved up to the Pacific Northwest where our crew met for a little coworking and a little friendly competition at Top Golf. Our team was able to meet one of our newest Echobind Babies and also enjoyed great food, and lots of laughs. ![Woman smiling and man holding baby](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/now2mbjnpzn3j3riyelh.jpg) ![Chicken and waffles on a plate](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/b88vuppfggktifwsz4su.jpg) ![Women playing golf](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/lfhd0cgbtkcwzzv1ciau.jpg) Next we flew south to the Sunshine State where our team gathered at [React Miami](https://www.reactmiami.com/) for some conference fun and learning, great food, and lots of laughs. (Are you sensing the theme here?) ![Company photo at dinner](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/hh8luaryztlpeyckcf3x.jpg) ![Two women holding signs at React Miami](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/h3sh6fnrzcdlpn7yr7ul.jpg) ![Company photo at lunch](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/ujcqqmfdmzvueprkhvbw.jpg) ![Table with seafood dinner](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/bt9ins3jbj5vvqqz5eqg.jpg) We kept the coastal theme going and headed to Brooklyn, NY. The first stop was obviously for bagels & lox, and then continued our theme of coworking, great food and lots of laughs. The view of the city from Harriet's Rooftop Bar did not disappoint! ![Board with a bagel, lox, and cream cheese](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/ciopoteyqrs08pgz3ziw.jpg) ![Men standing on New York roof top](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/wkr4lkkqhuonhcw6ophh.jpg) ![Women smile in front of water in New York](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/wkinawwyl5vuyjfpgjdl.jpg) ![Company photo at dinner](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/dyqha3xmrr3kminbxyne.jpg) Venturing back down south, but inland this time, a group of our engineering team went to [RenderATL](https://www.renderatl.com/) for fried chicken, hip hop music, and all things software engineering. The food, laughs and great company continued. ![Company photo at Render ATL](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/odqhdxmnglsfu6yfbecy.jpg) ![Hand holding food](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/ulaywvukqm4rymqdkh4w.jpg) ![Echobind logo on DJ booth](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/y8xeamc53ge7kcwpivjw.jpg) ![Company photo with swag](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/hbvpxkmyiv4ccbh4zdfn.jpg) We are stoked to plan our last, but not least, Spring(ish) Peak in KY before we all get together for our annual summit this fall. If things go as planned and stay on theme, this Peak may need its own blog post! ![Map of the United States](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/ylflscaupc2o3y6bvlqj.jpg) Our get-togethers are invaluable for our company and culture, and help strengthen connections so that our team can continue to thrive. Having a little fun never hurts, either. --- [View on echobind.com](https://echobind.com/post/spring-peaks-at-echobind) --- # Custom React Hook Guide: Build a Kanye Quote app with React Native > Let's reveal the closely held secrets around creating custom React Hooks with React with a lyrical twist. _By Mickey Martinez · 2022-05-19_ Today’s objective is to reveal the closely held secrets around creating **custom** **React Hooks** with **React.** Only a chosen few have been gifted this knowledge, so if you’re ready to elevate your game ride with me to the finish line. We’ll accomplish this with the help of Mr. West, the Yeezus of knowledge, by building a **React Native app** that hits a **public Kanye Quotes API** and returns data through a **custom React Hook**. Specifically, we’ll leverage the **`useEffect`** and **`useState`** Hooks, two of the most commonly used hooks in the [React Hooks API](https://reactjs.org/docs/hooks-reference.html). > _Hooks are functions that let you “hook into” React state and lifecycle features from function components. Hooks don’t work inside classes — they let you use React without classes._ If you’re new to React, the cool thing is that **Hooks** are the _**exact same**_ in **React Native** and **React.js**. With Hooks, we no longer need to write **class components** to manage state or query data — eliminating pesky lifecycle method bugs like `componentDidMount`, `componentDidUpdate` and `componentWillUnmount`. Instead this guide will pair **functional components** with **React Hooks**, demonstrating how to reuse stateful logic and manage side effects across components. Since the concepts of Hooks are platform agnostic, I’ve chosen to crank out a fully functional RN mobile app — focusing on `iOS`. Enough of the abstract talk, **buckle up cuz we about to [ball so hard](https://www.youtube.com/watch?v=gG%5FdA32oH44&t=30s) .. ya you know the rest!** ![Kanye West driving with Jay-Z](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/bduptucglrshgw7nu6qc.gif) ## Getting started ### API The API will be serving up a mix of certified Kanye West quotes, and I’ll be sharing a few of my favorite verses too. By the end you’ll have gone from College Dropout to the Graduation 🚀🐻. 🎵”_Ain't nobody expect Kanye to end up on top_ _They expected that College Dropout to drop and then flop”_ \- [\*Last Call](https://www.youtube.com/watch?v=cpbeS15sHZ0), The College Dropout\* Visiting the **[Kanye West API](https://api.kanye.rest/)** in your browser, you’ll see it returns a very simple JSON response of a randomized Kanye quote. Refresh and a new quote is displayed. Many times APIs will return a much larger JSON response, for example also including the album name, song title, release date, etc. For today, a simpler response will allow us to practice mastering the craft. ![Kanye West API screenshot](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/hoxk2bxgjhgj2hfac8pn.png) Viewing a quote from the Kanye West API endpoint in browser ### React Native `Expo` Build I’ll be going with the **Expo CLI Quickstart** option to create a new React Native project. 💡**Tip:** If you’re new to React Native, check out the **[Setting up the dev environment](https://reactnative.dev/docs/environment-setup)** docs. I recommend beginners **starting with the Expo CLI route** vs. React Native CLI. ![React Native guide screen](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/fltultni8pergdz2c1qv.png) **Open your terminal** of choice, make sure Node is installed, and then **install the Expo CLI command line utility tool** using `npm` or `yarn`. With Expo CLI installed, **create a new React Native project** named **`KanyeQuoteApp`** by running the following commands in your terminal: ```bash # Creates project named KanyeQuoteApp expo init KanyeQuoteApp ``` ![Expo code running in terminal](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/omt2of5mpotpemhmkemc.png) **Choose the** `blank` **template** and hit `enter`. _Expo will do the rest from here!_ ![Expo code running in terminal](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/y01vzznztuuudo54wphd.png) Now **switch into** the `KanyeQuoteApp` directory, **open VS Code**, and **start the development server**: ```bash # Navigate to project directory cd KanyeQuoteApp # Open project in VS Code code . # Start server. Equivalent to: expo start yarn start ``` ![App.js file in Visual Studio Code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/jld4sf4i7akicie2ilno.png) Viewing the default KanyeQuoteApp project in VS Code Since React Native projects are for mobile devices, you’ll need to **open an iOS simulator.** _If you develop on Android, launch an emulator with Android Studio_. To **open an iOS simulator** with Expo Go, **press `i` in the terminal**, or use the dev tools in the browser and click `Run on iOS simulator`. ![Expo options in terminal](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/w7jozzmvqbg9bvhcejpu.png) Expo Go CLI tools in terminal 💡**Tip**: _Make sure Xcode and Expo Go are installed. View the Expo docs on setting up [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) and [Expo Go](https://docs.expo.dev/get-started/installation/#2-expo-go-app-for-ios-and) app._ ![Sample app on iPhone](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/kh3m4aptwlidpdhcuf5o.png) Default iOS app on iPhone 12 Once launched, the default iOS app will look similar to the image above. A blank white canvas for us to go full Pablo Picasso on. So let’s **pop in the _Kanye West [Get Right for the Summer](https://www.youtube.com/watch?v=ylT16QB6Uig&t=47s)_ _workout tape_**, and really **switch the style up!** ![Kanye dancing gif](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/fx4mshsvkgkrtkjbhwpw.gif) **Grab the stylized template** from my [GitHub repo](https://github.com/CrypticMick/kanye-quotes-app-rn/blob/b877baf5f697f7aee83191878b16226b663042fc/App.js), **replace everything** in `App.js` and **Save.** _Expo Go should automatically reload the app whenever a file is changed._ If not, try `command + r` on device, or `r` in terminal. [Commit 1](https://github.com/CrypticMick/kanye-quotes-app-rn/commit/b877baf5f697f7aee83191878b16226b663042fc) ![Kanye West Quotes app home screen](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/jilg5oxg1xzxxeskbijg.png) Updated app with stylized template **Ayyy** that looks like the vibe I’m going for, now it feels like _we on an_ _[ultralight beam](https://www.youtube.com/watch?v=6oHdAA3AqnE)!_ ## Building out the `useEffect` With styling complete, **let’s focus on getting the `useEffect` Hook built out** and functional in `App.js`. Once it’s working as intended, we’ll then move that hook to its own file where it will be remixed into a custom hook — therefore _**accessible**_ and _**reusable**_ by our entire app. In `App.js` **import** `useEffect` from React: ```jsx import React, { useEffect } from "react"; ``` Now **add an empty** `useEffect()` above the `return` block: ![Code in VS Code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/betykqhfnefwuuu5mcqk.png) The `useEffect` hook takes a **callback** **function** as it’s first argument, and an **_optional_ dependency array** as the second argument. ```jsx useEffect( callback, dependency ) // dependency is an array ``` In our case, we’re starting with no dependencies, so your gut may be saying “we don’t need the optional dependency array.” While true, excluding the dependency array causes the effect to run with _**every render**,_ potentially creating unforeseen performance issues like an infinite loop\*.\* By adding an **empty [dependency array](https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects) `[]`**, we’re letting React know to run this effect _**only**_ _**when**_ the component **mounts** and **unmounts**. Within the callback function, **create an async function** named `fetchKanyeQuote` that **awaits** the response data from a `fetch` call to the Kanye API. More details on `async`/`await` [here](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async%5Fawait). Be careful to not make the callback function itself async, as that will cause issues with data calls being out of sync. Instead target your inner functions within the effect’s callback. ```jsx useEffect(() => { const fetchKanyeQuote = async () => { await fetch("https://api.kanye.rest/"); }; }, []); ``` Previously, we’d opt to **chain together a few Promises** — [promise chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using%5Fpromises#chaining) — to make use of the API data. You can take a look at that [approach here](https://github.com/CrypticMick/kanye-quotes-app/commit/6e30b4a87234793f7f4d2e308111ad4f40aed3f5). A **more modern approach** is to use `try/catch` blocks instead, assigning the responses to variables we can easily reuse. Below, `fetch()`returns a promise containing a JSON response that is assigned to the `res`var. Then, using the `json()`method we can pass that along as a JavaScript object that’s assigned to the `data`var. Check it out below: ```jsx // useEffect with a modern try/catch block useEffect(() => { const fetchKanyeQuote = async () => { try { const res = await fetch("https://api.kanye.rest/"); const data = await res.json(); console.log(data); } catch (error) { console.error(error); } }; fetchKanyeQuote(); }, []); // Promise chaining approach for comparison useEffect(() => { const fetchKanyeQuote = async () => await fetch("https://api.kanye.rest/") .then((res) => res.json()) .then((data) => { console.log(data); }) .catch((error) => { console.error(error); }); fetchKanyeQuote(); }, []); ``` To **confirm the Promise was successful**, you can `console.log` the `data` var and see the structure of the response. _Remember to call the `fetchKanyeQuote()`function at the end of the useEffect as seen above._ ![JSON object](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/tpayouvc2kgcmkoz4i9v.png) **In the terminal** we should see the simple structure of the `data` object: A `quote` property that has a radomized Kanye quote. To **access and display the quote**, we’ll need to tap into `data.quote` but first we need a way to save this quote in memory. [Commit 2](https://github.com/CrypticMick/kanye-quotes-app-rn/commit/869fc79f0a72397b3109a8dc8760af3c812678fa) 🎵”_How you say broke in Spanish? Me, no hablo”_ \- [Dark Fantasy](https://www.youtube.com/watch?v=UTH1VNHLjng), M.B.D.T.F ## Store Data Response in State (`useState`) So how do we store this quote (`data`) in memory — state in React — _and display it on screen?_ ![Kanye West gif](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/geq06trvgsqkau14pdte.gif) Well Sway, this is where the `useState` hook shines _bright like a [diamond](https://www.youtube.com/watch?v=rYuLutMe81k)_ 💎💎 **Import** `useState` from React and then we need to initialize state before the `useEffect`. The standard `useState` hook is structured like so: ```jsx const [stateVar, setStateVar] = useState(initialState); ``` In our case, the `initialState` of the `stateVar` variable should be set to an **empty string** `""`. Then we’ll use the **setter function** `setStateVar` to update the `initialState` value with an actual quote from the API. 💡**Tip:** With the `useState` hook, you never want to alter `stateVar` directly, instead always use the setter function. Doing so will not trigger a re-render. Since we can name `stateVar` anything we’d like, let’s go with `quote`. And `setStateVar` will be `setQuote`. ```jsx // Assigning initial value of quote var to an empty string const [quote, setQuote] = useState("") ``` Now **we can replace** `console.log(data)` with our **setter function** `setQuote(data.quote)`. This is the proper way to update the `quote` variable when using the `useState` hook. ```jsx import React, { useEffect, useState } from "react"; export default function App() { const [quote, setQuote] = useState("") // useState structure useEffect(() => { const fetchKanyeQuote = async () => { try { const res = await fetch("https://api.kanye.rest/"); const data = await res.json(); setQuote(data.quote); // setter function updates initialState value } catch (error) { console.error(error); } }; fetchKanyeQuote(); }, []); ``` At this point, we’ve made an async call to the API requesting a Kanye West quote and awaited the response. We then converted that to an object, and passed that value to our setter function which **updates** the `quote` var. Only thing left to do is **display the quote on device**. In the `return` block, within the `` component **replace the hard coded text** “_I Love You Like Kanye Loves Kanye”_ **with the** `quote` **variable** and **Save**. _The `quote` variable needs to be wrapped in curly braces `{}`._ ![Kanye West quote app next to Visual Studio code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/xwnezikyk2tc2isdxox6.png) Updating hard coded text to display an actual Kanye quote from the API via useState _[Lamborghini Mercy](https://www.youtube.com/watch?v=7Dqgr0wNyPo)_, your device should now be lookin’ fly to death and serving up fresh snippets of Ye. 🎵“_Let the suicide doors up_ _I threw suicides on the tour bus_ _I threw suicides on the private jet_ _You know what that mean, I'm fly to death”_ \- [Mercy](https://www.youtube.com/watch?v=7Dqgr0wNyPo), Good Music - Cruel Summer ![Kanye West gif](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/rv9ytwygwln8xzhtowzn.gif) This confirms both of our hooks are doing exactly what we want, fetching data and storing that data in state. For the moment, to fetch a new quote you’ll need to reload the app by pressing `r` the terminal. As a bonus I’ll eventually make the `More Kanye` button functional, so that it’ll fetch a new quote when pressed. Don’t _[runaway](https://www.youtube.com/watch?v=EMnQwBTJnMM)_ now, we’re on to the good part! [Commit 3](https://github.com/CrypticMick/kanye-quotes-app-rn/commit/286a46aa4d559816e4191dff44272da0ececa2b5)] ## Custom Hook 🪝 Here’s where we take our working logic and **remix it into a custom hook**. ![Kanye West gif](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/mpmxdqloqwnsastmqd0r.gif) > A custom Hook is a JavaScript function whose name starts with ”**use**” and that may call other Hooks **Create a new file** named `useRandomQuote.js` — the file naming here intentionally starts with `use` to signify that it’s custom hook. Scaffold out a **functional component named** `useRandomQuote` and **import** `useEffect` and `useState`: ```jsx // useRandomQuote.js import { useEffect, useState } from "react"; const useRandomQuote = () => { }; export default useRandomQuote; ``` **Reopen** `App.js` and cut out **the entire** `useEffect` block, as well as the line of code we need to manage state `[quote, setQuote]`. **Paste** this block of code within the **body** of the `useRandomQuote` function. ![Code running in Visual Studio Code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/cslvyikiv8y4ackdkonc.gif) Be sure to **return the** `quote` **variable within useRandomQuote** so that we can make its present value available wherever the custom hook is used. ```jsx // useRandomQuote.js import { useEffect, useState } from "react"; const useRandomQuote = () => { const [quote, setQuote] = useState(""); // useState structure useEffect(() => { const fetchKanyeQuote = async () => { try { const res = await fetch("https://api.kanye.rest/"); const data = await res.json(); setQuote(data.quote); // setter function to update initial state val } catch (error) { console.error(error); } }; fetchKanyeQuote(); }, []); return quote; // remember to add this line }; export default useRandomQuote; ``` **Reopen** `App.js`, **import the custom hook**, and assign it to a variable named `kanyeQuote`. ```jsx import useRandomQuote from "./useRandomQuote"; ``` ![File in Visual Studio Code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/wumax11z6ux8fkwencqa.png) Assigning the custom hook to kanyeQuote var Finally, **update the variable** from `quote` to `kanyeQuote` and **Save**. If a new quote renders like before, you’ve **successfully implemented a working custom hook**! [Commit 4](https://github.com/CrypticMick/kanye-quotes-app-rn/commit/2ff31d2b35157771eee88a265699368958819c51) 💡 _If the quote is blank, double check that you returned the_ `quote` _variable within_ `useRandomQuote()`. ![Kanye West quote app next to Visual Studio Code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/xwmbawd4r3xqdh3zyrko.gif) Using our custom hook to display a new Kanye quote That’s all it takes to share logic between components with React Hooks. As your app grows, the custom hook can be used again and again! We’ve covered how a `useEffect` helps us grab API data, store that data locally with `useState`, and then house all that logic in a custom hook. I hope **you’re feeling like a _[Monster](https://www.youtube.com/watch?v=pS6HRKZQLFA)_** at this point! I know _[I’m definitely in my zone](https://www.youtube.com/watch?v=gG%5FdA32oH44)_. Beyond custom hooks knowledge, you also have a modern React Native build — for some of you a first! \[[Completed project](https://github.com/CrypticMick/kanye-quotes-app-rn)\] If you’re thinking, _[**Nah Nah Nah**](https://www.youtube.com/watch?v=Mxs-ZMfyurU)_ he never showed us **how to make the button work**, I got you. Hit the **Bonus section** below. And if this is the end, \*_take it [Eazy\*!](https://www.youtube.com/watch?v=6kVZ5rsKLS4)_ 🎵”_The Lyor Cohen or Dior Homme, that's Dior Homme not Dior homie”_ _\-_ [Devil in a New Dress](https://www.youtube.com/watch?v=sk3rpYkiHe8), M.B.D.T.F ## **Bonus: Fixing the `Button` with `onPress`** At the moment, a **new quote requires restarting the app**. This would be terrible UX in an app, which is why I wanted to break down the solution. **The fix** requires something to trigger the `useEffect` hook to **refetch a new Kanye quote.** The new quote is then passed back to the `kanyeQuote` variable and finally updated on device. We can **tap into the** `onPress` **prop** in ``, and pass through a function that is triggered when the button is pressed. This function needs to tell the custom hook that we want a new quote. There are a few ways to make this happen. In my approach, I’ll be **passing a parameter** to the `useRandomQuote` hook, adding the param to dependency array, which will trigger the hook to run again if the param value changes. In `App.js` let’s **set up an iterator** with `useState` and then **pass the setter function** to `onPress` to update the value of `i`. ```jsx // Iterator to trigger custom hook const [i, setI] = useState(0); ``` Locate the `onPress` prop and add the `setI` setter function. ```jsx // Update onPress prop with setter function setI(i + 1)}> ``` Now **add the** `i` **parameter to the custom hook in order to** **re-fire the hook if the value changes** — this will happen when the button is pressed, as the value of `i` will change from `0` to `1`. ```jsx const kanyeQuote = useRandomQuote(i); // add i as a parameter ``` ![App.js file in Visual Studio Code](https://res.cloudinary.com/echobind/image/upload/v1661872445/echobind_blog/hxtdvxwn2hwipljtwnuk.png) Lastly, open up `useRandomQuote.js` and **receive the `i` param**, then **pass it to the dependency array**. **Save these changes** and test out the `More Kanye` button. Your button should be serving up fresh quotes each time it’s pressed! [Commit 5](https://github.com/CrypticMick/kanye-quotes-app-rn/commit/c4a873bd52e226899be87cd0997877e0ded24214) ![File in Visual Studio Code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/onahxearxgn343h41zgq.png) Receiving i param and adding to useEffect dependency array ![Kanye West quote app](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/q3lvy5hsrrm5xwb9grfh.gif) **That’s a wrap** on the bonus content. You’ve come along way with Ye! If you’re a fan, here’s [Yandhi](https://youtu.be/URJSG09Nm4s), an unreleased project of his. **Keep grinding** and soon you’ll be a _[\*\*Champion](https://youtu.be/L1SEEMkc-qw)_ of hooks\*\*. 🎵 “_So drive slow homie, you never know homie ... you need to pump your brakes and drive slow, homie” - Drive Slow, Late Registration_ --- [View on echobind.com](https://echobind.com/post/custom-react-hook-guide-build-a-kanye-quote-app-with-react-native) --- # Out and About at React Miami > Conferences are an excellent opportunity to learn new concepts and ideas, network with the community, and have a fantastic time with colleagues. _By Echobind Team · 2022-04-28_ ## **React Miami** [React Miami](https://www.reactmiami.com/) was this past week, April 18 - 19\. It was an opportunity our team did not want to miss out on, especially with the beautiful, warm Miami weather and beaches! Conferences are an excellent opportunity to learn about languages and frameworks, learn new concepts and ideas, network with the community, grow as an engineer, and have a fantastic time with colleagues. If you have never attended an in-person conference or have considered attending, we highly recommend it. ### **Meeting New People** Meeting new people tends to be scary for most people. There is untapped knowledge and resources out there for you, just waiting for you to go out of your comfort zone. Taking risks, regardless of the outcome, becomes a personal growth experience. Interacting with people at tech conferences is a great way to meet others in the same industry as you. Sharing ideas and knowing that other people have the same aspiring goals as you is refreshing. If you are not comfortable speaking with strangers, you can try the Pac-Man method. Typically at conferences, you will see a group of 3 or more. You can slowly approach the group and start listening to the person talking. It can be awkward, but you would be surprised at how often it results in a handshake and a short introduction. The method has not failed us, and it is an excellent chance to meet new people. ![photo of the echobind team at dinner after React Miami](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/n1qo6g0ffld2hzhfmxdg.jpg) ### **Goals for the Team** 1. Talking with the speakers after their talk. 2. Pictures of people we met. 3. Networking and letting companies know about Echobind and what we can do to help. 4. Look for talent, recruit new members to the Echobind team 5. Provide giveaways to the community. ### Our **day-to-day:** **Day 1:** Kicking off the first day of the event, we made our way to a brunch hosted by React Miami. The brunch was located at the [Marseilles Hotel](https://www.marseilleshotel.com/), with beautiful scenes of the famous Miami beach. ![Marseilles Hotel](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/rryp1oum6pzywgcb7pc8.jpg) We met some great people, and we spent most of our morning talking with other conference attendees. Brunch concluded after a couple of hours, and we headed to the conference. The team arrived early to get badges and check out the parent conference, [eMerge Americas](https://emergeamericas.com/). After our initial walk-through of eMerge Americas, we made our way to React Miami’s first talk by [Nader Dabit](https://twitter.com/dabit3), a Developer Relations Engineer at [Edge & Node](https://edgeandnode.com/). Dabit gave a fantastic talk on “The Technology Stack for the Decentralized Web.” Next up on the list was [Lee Robinson](https://twitter.com/leeerob), Director of Developer Relations at [Vercel](https://vercel.com/), who gave a talk on “Expectation vs. Reality of Marketing Websites.” His presentation structure captured the audience by showing live examples. Lee provided great insight into problems developers can run into when using frameworks other than NextJS. The team initiated conversations with various types of companies. Companies attending eMerge Americas ranged from start-ups to Fortune 500 companies. Notable companies attending the conference included [Toptal](https://www.toptal.com/), [Vonage](https://www.vonage.com/), and [Flatfile](https://flatfile.com/). Each had exciting booths and fantastic employees. The employees gave great insight into the company and how their product can increase workflow. The team continued to hand out stickers and business cards along the way, growing our partnerships and clients. **Day 2:** Day two kicked off with a 9 am arrival to eMerge Americas. The team was able to see the best tennis player in the world, [Serena Williams](https://twitter.com/serenawilliams), talking about all things web3. ![eMerge Americas](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/wmqtdrv57hywskepnpsi.jpg) We made our way to the React Miami conference happening next door to see the one and only [Kent C. Dodds](https://twitter.com/kentcdodds), Co-Founder, Director of Developer Experience at [Remix](https://remix.run/). Kent showed examples of how easy it is to get things running with RemixJS and why we should use it over other frameworks. Kent started to break down why this framework should be used in your next project. Another notable talk came from [Jenn Creighton](https://twitter.com/gurlcode). Jenn's talk titled “Debugging Async JS” highlighted the importance of knowing where to start. Jenn gave a fantastic insight into some pitfalls, going as far as showcasing some unique ways to debug async javascript. After Jenn’s talk, the team heard an inspirational talk from [Jerome Hardaway.](https://twitter.com/JeromeHardaway) Jerome is a self-taught developer who founded [Vets Who Code](http://vetswhocode.io). Jerome pairs with American Veterans, teaching them how to code and providing vets the opportunity to get back into the workforce. We had the opportunity to learn more about accessibility from [Isabela Moreira](https://twitter.com/isabelacmor), a Software Engineer at [Microsoft](https://www.microsoft.com/en-us/). Isabela did a great job informing us of resources to make websites more accessible and how we can use that knowledge to pass accessibility audits for the future. Isabela shared how frustrating it can be for someone when a website is not accessible to them. As a new engineer in the industry, it was an eye-opener and something that should be important to the developer community to ensure our websites and applications are accessible. It’s great to work with a team that already keeps accessibility in mind, helping clients with audits and implementing best practices. The team enjoyed various talks for the rest of the day. We left with a deeper understanding of web3 and made new friends! **Day 3:** The team spent the final day of our trip co-working in one of CIC Miami’s amazing office spaces. [CIC Miami](https://cic.com/miami) borders Miami's famous Wynwood district. ![CIC Miami office](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/d0lj564wksc1c8wdfhef.jpg) Wynwood Art District - Miami, FL ![Wynwood Art District](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/gzv3sd5ubuvzlvqgrmi4.jpg) The team had the opportunity to co-work together, which is refreshing because Echobind is a remote-first company—sharing ideas, pair programming, and talking about what we enjoyed during the conference and how we can implement some of those ideas in our work. **Echobind** If you are looking to grow your career, Echobind is currently hiring. Click [here](https://echobind.com/careers) to learn more about Echobind. **Conclusion:** We hope our journey and insights help you when attending a conference in the future. The takeaway is to try to get out of your comfort zone yet feel comfortable. Thank you for reading, and we would love to hear about your experiences at conferences. ![Echobind React Miami logo](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/jgrnfci5m8s3iwo5ssbe.png) Authors:[ Dennis Campos](https://echobind.com/team/dennis-campos) and [Preston Kelly](https://echobind.com/team/preston-kelly) --- [View on echobind.com](https://echobind.com/post/out-and-about-at-react-miami) --- # Prioritizing Solutions Over Features > Zack Marty shows how the way in which companies can sometimes prioritize features over solutions. _By Zack Marty · 2022-04-06_ In this short video I show an example of the way in which companies can sometimes prioritize features over solutions. Before someone says that they need an app to solve a problem, fully understanding the basics of the problem is necessary to arrive at a real solution that lets you discover the needed features. At Echobind, finding the solution to a problem takes priority over cranking out features. A customized solution requires a strong discovery process that fully immerses itself in the problem’s context.
When you understand the problem fully, the solution almost comes through osmosis. --- [View on echobind.com](https://echobind.com/post/prioritizing-solutions-over-features) --- # Reduce repeated code in Jest tests with it.each > Reduce repeated code in Jest tests with it.each. This approach allows users to cover more ground with test cases! _By Kishan Gajera · 2022-03-31_ When writing tests, I often find myself repeating test cases with only minor changes. A common example of this is when testing different values for a form or API endpoint and expecting certain validation errors. Below is an example of what I'm referring to. These are two tests that are testing a GraphQL mutation to log a user in. ```jsx describe('invalid email', () => { it('returns an Authentication error', async () => { await UserFactory.create({ email: 'foo@wee.net' }); const variables = { email: 'fake', password: 'fake' }; const response = await graphQLRequest({ query, variables }); const errorMessages = response.body.errors.map((e: GraphQLError) => e.message); expect(errorMessages[0]).toBe("No user found for email: fake"); }); }); describe('invalid password', () => { it('returns an Authentication error', async () => { const user = await UserFactory.create({ email: 'foo@wee.net' }); const variables = { email: user.email, password: 'fake' }; const response = await graphQLRequest({ query, variables }); const errorMessages = response.body.errors.map((e: GraphQLError) => e.message); expect(errorMessages[0]).toBe("Invalid password"); }); }); ``` Both of these tests do the following: 1. Creating a user in the database 2. Make GraphQL request for the login mutation 3. Check the error messages returned from the server The only difference between the two tests are the variables passed to the GraphQL mutation and the expected error messages. Another approach to writing the tests to reduce code repetition is using [it.each](https://jestjs.io/docs/api#testeachtablename-fn-timeout). This allows us to write the test once and loop over it using different data in each iteration. Using this approach, our tests could be rewritten like this: ```jsx it.each([ { variables: { email: 'fake' }, error: 'No user found for email: fake' }, { variables: { password: 'fake' }, error: 'Invalid password' }, ]) ('returns error: $error', async ({ variables, error }) => { const user = await UserFactory.create({ email: 'foo@wee.net' }); const response = await graphQLRequest({ query, variables: { email: user.email, password: 'fake' , ...variables, } }); const errorMessages = response.body.errors.map((e: GraphQLError) => e.message); expect(errorMessages[0]).toBe(error); }); ``` Now, we have an array of test cases passed to `it.each` that specify the variables to use in the GraphQL mutation and the expected error message. This is the array that will be iterated through. So, our test will run for each element and the element will be accessible in our test. You should also notice that we're using `'returns error: $error'` for the title of the test. The `$error` references the `error` property in our test case objects. We do this to give each test case a unique title so if it fails, we know which case caused the failure. We can easily add more test cases without needing to copy and paste. In this example, it might also be good to test when the `email` or `password` are undefined. To do this, we would just add the following elements to the array: ```jsx [ { variables: { email: undefined }, error: 'Email is required' }, { variables: { password: undefined }, error: 'Password is required' }, ] ``` I hope you saw how using this approach, you reduce repeating code and also can cover more ground with your test cases! --- [View on echobind.com](https://echobind.com/post/reduce-repeated-code-in-jest-tests-with-it-each) --- # TypeScript - Generic Types > This is a tutorial on what generic types are in TypeScript, how to use them, and when you might find a use case for them. _By Dominic Sherman · 2022-03-30_ Generic types provide a way to dynamically define types within TypeScript so that you can more accurately type functions and helpers. This is a tutorial on what generic types are, how to use them, and when you might find a use case for them. This assumes you have a basic understanding of TypeScript and are looking to level up your knowledge. ### Example - Removing Nulls from Arrays Suppose you had a common problem in your application with arrays containing null values. This could be data coming from your server or somewhere else, but it can be a very annoying problem to deal with because then every time you want to use this array, you have to check every value for nullability. For example: ```javascript const res = await fetch('/foo'); const data = await res.json(); return data.map((data) => { if (data) { // do something } else { // what do I do??? } }); ``` Most of the time, we would probably just filter out any values that are `null` and then return the data. ```javascript const res = await fetch('/foo'); const data = await res.json(); const dataWithoutNulls = data.filter((data) => data !== null); return dataWithoutNulls.map((data) => { // do something }); ``` This works fine, but not something we’d want to do every time we need to fetch data. So we’d probably add a helper function for it, and maybe add it to a fetch middleware or something like that. ```javascript const removeNulls = (dataWithNulls) => { return dataWithNulls.filter((x) => x !== null); } const res = await fetch('/foo'); const data = await res.json(); const dataWithoutNulls = removeNulls(data); return dataWithoutNulls.map((data) => { // do something }); ``` If we introduce TypeScript to this scenario, that makes things a little bit more complicated. If our requests are strongly typed, which is common and easy now with [GraphQL](https://www.graphql.org) and [codegen](https://www.graphql-code-generator.com/), then we have to make sure our function to remove nulls is also properly typed. Let’s try this without adding any types to our function first. For the example, we’ll set up some data that can either be an interface type or null: ```javascript interface FooType { foo: string; bar: number; } const withNulls: (FooType | null)[] = [{ foo: "foo", bar: 1 }, null, { foo: "bar", bar: 2 }]; const value = withNulls[0]; ``` If I try to pull a value out of my `withNulls` array, TypeScript knows it might be `null` (hovering over `value` shows that its type can be `FooType | null`): ![Helper Function](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/axiewgowd6sw2vwlizzl.png) Let’s add our helper function: ```javascript const removeNulls = (dataWithNulls) => { return dataWithNulls.filter((x) => x !== null); } const withoutNulls = removeNulls(withNulls); const nonNullValue = withoutNulls[0]; ``` If we look at the type on `nonNullValue`, its shown as `any` now: ![nonNullValue typed as withoutNulls[0]](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/rrbngtxopkizn0klxx1c.png) Since we don’t have any types on our function, TypeScript will just default everything to `any`. While this “works”, [it defeats the point of TypeScript](https://thoughtbot.com/blog/typescript-stop-using-any-there-s-a-type-for-that) because now whenever we use the function we lose all context as to what the type was. At that point, we are essentially writing JavaScript. Let’s try to add some types to our function. For the inputs and outputs of our function, we want it to receive an array that is either the `FooType` or `null` , and then return an array that is only `FooType`: ```javascript const removeNulls = (dataWithNulls: (FooType | null)[]): FooType[] => { // ... } ``` Great, that looks right, and it looks like it worked to properly maintain the types after being run through the function (hovering over `nonNullValue` shows its value to be `FooType` rather than `FooType | null`: ![const nonNullValue is now FooType](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/zcxijbfx9ntwzpafq9io.png) While our function definition works now, we’ve introduced a problem within our function itself. The`filter` function doesn’t know how the logic of our predicate is working, so it thinks even after filtering the type of the function hasn’t changed and doesn’t match what we’re saying we’re returning. Here’s the error we’re getting: ```javascript Type '(FooType | null)[]' is not assignable to type 'FooType[]'. Type 'FooType | null' is not assignable to type 'FooType'. Type 'null' is not assignable to type 'FooType'.ts(2322) ``` ![TypeScript code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/edufnsmtihrc3zukdjlt.png) In order to properly type this, we need to define a predicate type as the return value of our filter function, which looks like this: ```javascript const removeNulls = (dataWithNulls: (FooType | null)[]): FooType[] => { // predicate on callback return dataWithNulls.filter((x): x is FooType => x !== null); } ``` Looks like everything is working now, but as I’m sure you’ve noticed, this function only works for `FooType` What if we needed to do it for a different type? This is where generic types come in. Without them, we’d have to write a new function for every type that we want to use, which kind of defeats the purpose of the function. Our goal here would be to write a utility function that allows users to consume it while using their own types. If we review TypeScript’s documentation, we can see that generics are the way to do properly do this. TypeScript allows for something called a type variable, which is a special kind of variable that that works on types rather than values. From the `Hello World of Generics` TypeScript docs, we can see the basic identity function usage of type variables: ```javascript function identity(arg: Type): Type { return arg; } ``` This type variable Type ”allows us to capture the `type` the user provides (e.g. `number`), so that we can use that information later”. Let’s try to use a type variable in our function to maintain the type passed by the user. Our goal with our type variable would be to receive our argument as an array of our type (`T`) or `null`, and return an array of just our type (`T`). That would look like this: ```javascript const removeNulls = (dataWithNulls: (T | null)[]): T[] => { return dataWithNulls.filter((x): x is T => x !== null); } ``` Since we have the argument `dataWithNulls` defined using our generic type variable `T`, we can either explicitly pass `T` when calling the function or let TypeScript infer it for us. ```javascript // valid const withoutNulls = removeNulls(withNulls); // also valid // since withNulls is type (FooType | null)[], TypeScript knows that T must be equal to FooType const withoutNulls = removeNulls(withNulls); ``` Now that we are using our generic type, we can double check our example to make sure it still works. Looking at the type of `nonNullValue` from above, we can see that its type is still just `FooType` instead of `FooType | null`, except now our function works for more than just `FooType` 🥳 ![TypeScript code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/nl2x3nsjuqoqqfvkp4s2.png) Now that we have a basic understanding of generics and why they can be useful, let’s look at a more complicated example. ### Example - Removing Nulls from Objects Suppose I wanted to remove the nulls from the values of an object. This is a similar problem, but requires a much different solution. Here’s my example type: ```javascript interface BarType { [key: string]: string | number | null; } ``` I’d like to write a function that removes any fields that are null off of it and returns the type correctly. Our first pass might look like this: ```javascript interface BarType { [key: string]: string | number | null; } interface BarTypeWithoutNulls { [key: string]: string | number; } const removeNulls = (objectWithNulls: BarType): BarTypeWithoutNulls => { return Object.keys(objectWithNulls).reduce((acc, key) => { const value = objectWithNulls[key]; // only add to accumulator if value is not null if (value !== null) { acc[key] = value; } return acc; }, {}); } ``` The JavaScript looks correct, and the types “work”, but as we found earlier it only works for this specific type and we have to redefine our new type without nulls. In order to define a type of an object that removes the null, we’ll need our **type** to receive a generic type. Something like this: ```javascript type ObjectWithoutNulls = { // now what?? } ``` Our goal here is to recreate our generic type `T` in this new type but make sure we are excluding the nulls. What this means is that we want our type to map over all of the keys in `T`, and set the value of our type to be whatever the value of the generic type would be, except exclude `null`. If we look at [TypeScript’s documentation on mapped types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html), we can see that the syntax for that looks like this: ```javascript type ObjectWithoutNulls = { [K in keyof T]: // get the value? } ``` Since we have `K` as a key of `T`, that allows us to just pull `T[K]` to get the type value off of `T`: ```javascript type ObjectWithoutNulls = { [K in keyof T]: T[K]; } ``` All we’ve done now though is create an identity type, which gives us back whatever we give it. If we want to exclude nulls from the value on `T`, we need to take advantage of the `Exclude` type built into TypeScript: ```javascript type ObjectWithoutNulls = { [K in keyof T]: Exclude; } ``` Another thing we need to do for this type is to enforce what types can be passed to it. Going to the [TypeScript documentation on generic constraints](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints), we can use the `extends` keyword to achieve this. We want our type to accept any generic type that is a [`Record`](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type) with keys that are anything or `null`. We can use `keyof any` here, which generates a type that can be any of the base types (`string | number | symbol`). It is important here to note the difference between `any` and `keyof any: any` will allow anything to be passed (even another object), while `keyof any` just breaks down to only allow `string | number | symbol`. The final type looks like this: ```javascript type ObjectWithoutNulls> = { [K in keyof T]: Exclude; } ``` Now we can use our type as the return value of our helper function: ```javascript // use the same generic constraint that we use in ObjectWithoutNulls const removeNulls = >(objectWithNulls: T): ObjectWithoutNulls => { // cast Object.keys(type) to be keyof T since it generates a string[] by default return (Object.keys(objectWithNulls) as (keyof T)[]).reduce((acc, key) => { const value = objectWithNulls[key]; // only add to accumulator if value is not null if (value !== null) { // value is still listed as keyof T here, so we need to exclude null now that we've checked for it acc[key] = value as Exclude; } return acc; }, {} as ObjectWithoutNulls); } ``` Now let’s set up an example to test it out: ```javascript const withNulls: BarType = { foo: 'foo', bar: null } const value = withNulls['foo']; ``` If we look at the type of `value`, we can see that it can be `string | number | null`, as expected: ![TypeScript code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/wdmlyijjvdhiuygouorq.png) When we run this through our `removeNulls` function, we can see that a value off of the object after removing nulls is just `string | number` 🚀 ```javascript const withoutNulls = removeNulls(withNulls); const nonNullValue = withoutNulls['foo']; ``` ![TypeScript code](https://res.cloudinary.com/echobind/image/upload/v1661872444/echobind_blog/g0vx1wenyps9bxvh5zt1.png) We can also double check that our generic constraint works correctly and only allows objects, by trying to pass `[null]` to the function. We see the following error, showing that we have this properly set up: ```javascript