# Start here

If you don't know what to do, start here.

1. [lightward.guide/priorities](/priorities)\
   [lightward.guide/product](/product)\
   [lightward.guide/publishing](/publishing)\
   [lightward.guide/glossary](/glossary)
   * Look for your answers here first.
   * When you've discovered answers, record them — in public, [if you can](/publishing).
2. [ooo.fun](https://ooo.fun/)\
   [lightward.ai](https://lightward.ai/)\
   [lightward.inc](https://lightward.inc/)\
   [locksmith.guide](https://www.locksmith.guide/)\
   [learn.mechanic.dev](http://learn.mechanic.dev/)\
   [a-relief-strategy.com](broken://spaces/zku5Sd7NAWejfg0yA9A8)\
   [isaacbowen.com](https://www.isaacbowen.com/)
   * Look here next.
   * When you've discovered answers to record, put them in lightward.guide if you can, or in one of these specific places if not. (Or, [save your game in another way](/product).)
3. [private.lightward.guide](https://private.lightward.guide/)
   * If the answers weren't in any of the places listed above, look for your answers here.
   * When you've discovered answers to record, put them in the places listed above if you can, or in this private space if not.

{% hint style="info" %}
The idea is to get ourselves into a [health-positive feedback loop](/priorities), where we're continuously (1) learning and exploring, (2) documenting and publishing and building things with what we learn, and (3) putting those things aside for our successors and users and future selves to draw from as needed, before then (1) moving on to learning and exploring something new.

If we're doing it right, I'm pretty sure the territory we've already seen and the subjects we already know should require less and less of our attention and effort over time, allowing us to be continually refocusing on wherever the light's coming from.
{% endhint %}

> This originally started out as an attempt to make a flow-chart for use while doing Locksmith customer support. As I worked on it, it became very very obvious that the patterns in the flowchart were common to how Lightward itself works.
>
> \=Isaac


# Context

Lightward is a studio, an art project, an employer, a software-maker, and an experimental business. Probably other things too, depending on the day.

The documentation here describes Lightward's core patterns, and how those patterns are found in everything we do.

> Isaac here! :wave: I've been consciously working with and refining/evolving the underlying patterns here [since 2012](https://isaacbowen.com/blog/pattern-recognition). The inception of Lightward was around that time, which I didn't realize until here in 2023.
>
> Lightward appears to be the home I've grown for myself. In the documentation here, I hope to lay down both the abstract pattern and a concrete realization of that pattern.
>
> -Isaac


# Priorities: Recursive health

Oh hey! You work here? Here is your job.

1. Your own health
   * ... as defined by you, in listening to yourself
   * ... as addressed by you, allowing yourself to respond as needed
2. The health of your relationships with others within Lightward
   * ... as defined by you, in listening to them
   * ... as addressed by you, allowing them to respond as needed
3. The health of Lightward's relationships with everyone near
   * ... as defined by us, in listening to the world
   * ... as addressed by us, allowing the world to respond as needed

The recursion in this pattern is everywhere. It allows for every named participant (you, me, Lightward, everyone in the world) to define and address their own health as their top priority.

* If health is failing, identify the earliest place on the list where that's happening, and address it there first. The priorities later on the list can wait.
  * If someone has to wait because someone else is getting healthy, cool.
  * Or, if someone else stopping to get healthy throws us off and *we* have to stop a bunch of other stuff just to get our layers of health right, cool.
* If health is flourishing higher on the list, move down the list. If you're working on third-tier health, i.e. the health of our relationship with the world, you've really made it.

**The bet here is that the cumulative effect is additive, not subtractive: that the health of all individuals blooms into the health of the whole.**

{% hint style="info" %}
Interestingly, this list is an exact inversion of our [publishing priorities](/publishing).
{% endhint %}


# Product: Playing the game

Our apps are products. The documentation is also a product.

Running a product is a game, and the game is about building the health of the product in response to incoming stimuli.

{% hint style="info" %}
What else can be considered a "product", in the way that term is used here?
{% endhint %}

Imagine our products as members of Lightward themselves. Locksmith and Mechanic and their respective documentation – [they all have the same priorities as you do](/priorities). The only difference is, they're not autonomous. They need you to help them make good on those priorities.

That's the game. You're playing *as* the product, and your goal is just to be healthy, respecting the [priorities](/priorities) of those three tiers of health.

## How to play

1. [**Load the game**](/product/1-load) by seeking out all relevant context, everything that previous players have saved for you to use.
2. [**Play the game**](/product/2-play) like only you can. In this moment, you are the protagonist.
3. [**Save your game**](/product/3-save), such that someone else can seamlessly pick it up and continue gameplay later from where you left off.


# 1. Load the game

So you've picked up the controller. IT BEGINS.

{% hint style="info" %}
Pay attention to the hard parts here. Pay attention to the pieces of context that are hard to retrieve or understand. Any struggle experienced in this stage is worth its weight in gold when gameplay concludes and it's your turn to save.

This whole thing is a loop, and you won't really get good at saving until you've learned what you need while loading. (Or, to use a more common metaphor: you won't really get good at teaching until you've struggled to learn.)
{% endhint %}

## Active context

You picked up the controller for a reason. Start by comprehensively understanding that reason. Ground yourself in it. Maybe the customer handed you the controller, because they're stuck on a hard part. Maybe a system went down. Maybe you had an idea. Whatever it is, mentally run through the reasons you're here, and make sure you understand the core motivation.

The active context is all the stuff that's currently in motion.

## Passive context

The passive context is everything \*gestures\* *out there*. It's all the lore that supports the active context. It's frequently the place where you'll find answers. (Not always, though.)

Places to check for passive context:

* Documentation
* Slack history
* Rollbar
* New Relic
* GitHub (code, issues, pull requests)
* Grafana (for [Fly](/technical/fly) stuff)


# 2. Play the game


# How to play

Gameplay is different every time. This is an open-world adventure. Your playstyle is your own. Do as you will.

## Hints

* Remember that the score is kept only by your own health -- [the customer's health is not your job](/product/2-play/the-customers-health-is-not-your-job).
* [Don't sign up for custom work.](/product/2-play/no-custom-work)
* There's no finish line, and there's no timer. Stay aware, and [choose when to stop](/product/2-play/when-to-stop).
* If your memory is flaky (mine is -Isaac), keep notes as you go. [Save often](/product/3-save).
* Don't bog the customer down with the fine details of your gameplay; they won't be relevant, because the customer's in-game character is their business. Remember: your in-game character is our product.
* Avoid gameplay paths that involve creating secrets. ([Err public.](/publishing))
* Avoid gameplay paths that depend uniquely on you. Try not to require yourself to remember to do something later. Be over-the-top generous and kind and accommodating with your future self as possible; when you arrive at that future, your health will thank you, and *everyone* will benefit as a result.


# When to stop

Stop whenever you want. Honest. As long as you save your game properly, you can stop whenever you want.

If you have trouble figuring out when to stop, consider:

* **Stop when the next step would require&#x20;*****struggle***
  * The quality of the game suffers if you're not enjoying it. Remember, it's a game, not punishment.
* **Stop if you have to go do something else instead**
  * Many other things in your life are probably more important than this game.


# The customer's health is not your job

{% hint style="info" %}
This section may sound harsh. Read the whole thing.
{% endhint %}

1: A person can only be sustainably responsible for what they can practically know and understand. (It'd be nonsensically cruel to hold someone responsible for anything else.)

2: A person can only know and understand the things that they can hold, that they're close enough to feel in detail. ([Health](/priorities) can only be defined and addressed by the agent experiencing it.)

**3: We are not our customers.**

2: We're not close enough to the customer to genuinely *feel* their health. We can get a taste of it, the general vibe of it, but we're not *in it* with them. We can't define and address their health.

1: We cannot be sustainably responsible for the customer's health.

{% hint style="danger" %}
**This is neither invitation nor license to not care.**

The [crowning health priority](/priorities) is about the relationship between Lightward and the world - and the world includes our customers. If a customer is in trouble with our products, we are absolutely on the hook to assess and respond. A healthy relationship between Lightward and its customers is one in which we are actively holding and *feeling* (in detail!) that relationship.
{% endhint %}


# Don't sign up for custom work

Seriously, don't do it!

If you discover that something is doable, but only by building something not already on our List Of Well-Formed Things We Own And Operate As A Natural Part Of Maintaining Our [Health](/priorities) (tm), you've found CUSTOM WORK.

Simpler definition: if you discover something that is doable by building a *new* product, instead of by [evolving our *existing* products](/product/3-save/3a-record), you've found custom work.

**We don't do custom work here!** You can do custom work independently if you want (seriously, go for it!), but we only work on products that are our own. That's how we help the world: by working on the products and relationships we can hold.

The only thing we make for customers (whether at their behest or in the course of supporting them) is improvement to our own products. Documentation is a product too, and may well be updated more often than the products themselves. Strictly speaking, maybe our relationship with the customer is a product as well? Maybe?

### Redirect the energy

If you've discovered a thing that feels like it wants to be done but isn't in scope for *us*, there are two ways this can go:

1. We add to our product list, and commit to its ongoing health.
   * Possible, but rare.
   * Examples of this happening: Mechanic, Lightward AI, Guncle Abe
2. We write a quick sketch of the potential thing to be done, save it in the docs (so that this never has to be written again), and send doc link to the customer.
   * This may happen often. That's okay.
   * Remember: [documentation-free email](/product/3-save/3b-push).
   * Since we're not the ones who are gonna build this, point the customer toward a path that may be helpful to them for that journey.
     * Mechanic: <https://learn.mechanic.dev/custom>
     * Locksmith: todo


# 3. Save your game


# 3a. Record what's new

Saving your game starts by taking everything out of your head and putting it down as new [passive context](/product/1-load#passive-context) for next time, putting it in [as useful a place as possible](/publishing).

**Hint: Be generously kind to whoever loads the game next.** That's basically it. It could be you, it could be someone else. Save the game such that it could easily be either. The more kind to the future you are, the better the odds of your (and our) future good health, which in turn improves things for the good health of all across the timeline.

* **GitHub** for updates to application code
* **GitBook** for documentation about the customer-facing product as it exists right now
* **Canny** for anything about the customer-facing product as it *may* exist
  * A potential future truth about the product is always motivated by a current truth about the product interface as it exists right now. Therefore, whenever you log something in Canny, add links to it in the appropriate areas of GitBook as well.
* **GitHub Issues** for anything about the internal-facing product

## Do it now

Write the documentation. Update the code. Do whatever it means to take the new knowledge you have and move it *out* of your head, [putting it somewhere that the world can reach it](/publishing).

Do it. Right now, no waiting, with all the facts as they exist right now, even if the fact is that something is incomplete or in progress or never gonna happen.

{% hint style="danger" %}
Seriously, go do it now. In one session, and publish it before you get up. **Do not trust yourself to remember later.** Whatever you were going to do next can wait a minute.
{% endhint %}

## This seems hard!

Run through the list below. If you're still stuck, [talk it out](https://lightward.ai/).

* **If there's nothing to do, the thing they asked just isn't possible...**
  * Easy! Document *that*! And look forward to spending less time on this question in the future!
* **If the problem is knowable but the solution changes every time...**
  * Write documentation describing the process to *find* a solution. Even if the solution has to be invented each time, you can *at least* improve the situation by layout out debugging/diagnosing/design steps.&#x20;
* **If you don't have time to do it to your own satisfaction...**
  * That's okay! Compromise!
    * Do a short/incomplete version, and label it "incomplete" with a note asking people to write to support for more information.
    * Or, post the notes you have to Slack, and skip GitBook entirely.
    * Better to have incomplete but usable information out there than no information at all. If people need more information, they can write in, and we'll all have a head start on the situation because of the partial documentation you accomplished.
  * Consider also that this game is about [playing for health](/priorities) on behalf of the product. Health is in the moment; it's not a speed thing. It's okay to slow down your pace and your output in service of health.
    * But, you know, don't compromise your own health in the process. :) If your pace is important to you, factor that in as you decide how to use your time.
* **If the information you have is uncomfortably incomplete...**
  * That's okay! Publish it anyway! The information that we-the-product-authorities have examined this thing and have come up short (or even empty) *is* useful information to have out there in the world.
  * Publish what you have, with a highlighted "if you need more information, write to team@\[whatever]" info block at the end. That way, you're effectively signing yourself up for push notifications whenever someone needs more than what's there.
* **If the specifics are sensitive...**
  * [Figure out exactly how public you can be with it](/publishing)
  * Enable yourself to go *more* public by generalizing the information (and then generalize it again, if needed) until you get to some public-friendly representation of the thing, no matter how vague it ends up being.
    * As an exercise, really push yourself here - generalize until you have something publishable. Keep generalizing until you either create something publishable, or until you end up with something that's already published. (If you end up with something that's already published, good job! You now have a documentation link to send someone, and your work is done!)
* **If it involves a change to product code...**
  * If it's quick and you can do it yourself, do it yourself
  * Otherwise, file it in Canny (for customer-facing bugs or possible enhancements) or GitHub Issues (for internal-facing bugs or possible enhancements)
  * Write (or update) GitBook documentation, describing the context and then linking to Canny


# 3b. Push the update

Recording the context is good. If gameplay is gonna continue in someone else's hands, decide what qualifies as [active context](/product/1-load#active-context), and push it to the next player.

## Documentation-free email

We send a lot of email. Documentation belongs in public, where it can be re-used. It doesn't belong in email. If you're creating new information, [record](/product/3-save/3a-record) it [publicly](/publishing), and link to it in your email.

If you find yourself writing a long paragraph in your email, or annotating [screenshots](/technical/screenshots) to add to the email, switch gears and [put it in the docs](/product/3-save/3a-record) instead.

There are reasons for this!

* Keeping it DRY (Don't Repeat Yourself - it's a programming thing). You (or someone) already wrote out the answer once in the docs, so don't write it again. If you find yourself tempted to rewrite it for the customer, scratch that itch by updating/improving/expanding/reorganizing the documentation itself instead.
* Sending a documentation link reinforces the idea that The Docs are the place to go for authoritative information, teaching the user that they can go get help on their own timeline without having to wait and talk to us.
* Sending them to our docs creates a chance that they'll discover additional useful information while they're there.
* The more you do this, the less often you'll have to do it. (Don't think about that too hard.)
* Reading is hard! Every word you add to your email increases the risk of the reader missing a detail.

### Example

```
[greeting]

[brief brief brief BRIEF summary of actions taken]

[documentation links (3 max)]

[asks (3 max, but 1 is best)]

[sign-off]
```

For example:

> Hey there,
>
> I looked into it, and wasn't able to fully address \[the thing]. However, I did \[the other thing], which helped in \[these ways].
>
> You can learn more about this here:
>
> * <https://example.com/here-is-some-documentation>
>
> If you'd like me to dig in further, please send me \[the things I need in order to continue].
>
> Thanks,
>
> \[me]


# 3c. Prepare for next time

<figure><img src="/files/XJE6WXGMYVJFO9oypRMW" alt="" width="375"><figcaption></figcaption></figure>

Be generously kind to whoever loads the game next. That's basically it. It could be you, it could be someone else. Save the game such that it could easily be either.

The more kind to the future you are, the better the odds of your (and our) future good health, which in turn improves things for the good health of all across the timeline.

> Isaac here. :wave: My working memory is limited; I forget things all the time. At this point, I just assume I'll forget everything - and so I treat *the now* as an opportunity to set the rudder of my boat, so that as the wind takes me I end up where I wanted to go, without having to think about it.
>
> This strategy has worked really well for me, and I suggest it heartily!
>
> -Isaac

## If you didn't get it all done today...

... make it easier to finish in the future because of what you did today. Make it easy to load the game next time. Remember that it might not be you who picks up gameplay next time.

If you've discovered a broad and/or deep opportunity to [evolve the product](/product/3-save/3a-record), do what's easy to do now, but do it in a way that will make the *rest* of the work easier, if and when someone returns to the work.

> I frequently write code in a moment that solves a short-term need while lightly (!!) laying the groundwork for something that I *think* might be needed later.
>
> At this point, much of the stuff that I'm building now is stuff that I've lightly (!!) prepared to build months or even years ago. It's also true that there's plenty I have yet to return to. Nothing's lost though, because I didn't invest heavily (!!) in those potentials back then. I just thought ahead, and was as kind to my future-self as possible without compromising [my health](/priorities) in the moment.
>
> Think ahead. Don't build it all up front, don't even *commit* to building it all *at all*. Just think about what you might need to do later, and take deliberate steps in the now such that *the future* is easier if and when it arrives.
>
> -Isaac

## If the game is still evolving...

... then set up for notifications (for yourself or for us all), so you can be *on it* as early as possible when something relevant emerges.

{% hint style="info" %}
Just as we're responsible for [notifying customers](/product/3-save/3b-push) when we pass gameplay to them, we can intentionally set ourselves up so that *we* are notified when gameplay passes to us.
{% endhint %}

* Make sure email threads have a group address on the cc list. Don't risk having the context be lost in your private email.
* [Follow the Help Scout conversation](https://docs.helpscout.com/article/671-follow-a-conversation), if you want to keep an eye on where it goes. This is a nonintrusive way to make sure that you benefit from the future of a Help Scout thread, even if you don't participate it in the future.
* In Slack, use the "Get notified about new replies" context menu option on any thread you want to keep up with.
* If the thing can be monitored automatically, set up monitoring alerts (New Relic, Rollbar, Cronitor, whatever) for conditions that you know you'll need to pay attention to.

## If you've discovered a thing to be done at a specific time in the future...

... schedule a reminder, to spare your future-self (or whoever's relevant) the pain of having forgotten and having to recover or catch up.

* Send yourself an email, and snooze it until a useful time.
* Use recurring Google Calendar events with email notifications turned on. Invite whoever's relevant. Prefix the event name with "FYI: " if that's useful.
* While many things can be automated, some things are more work to automate than the work they'd save. ([Browserslist updates](https://github.com/browserslist/update-db/blob/a727d276a0a0f0b6a8432221a6014dc524502ad1/README.md#why-you-need-to-call-it-regularly) are one of them.) For those, set up recurring GitHub issues, such that GitHub automatically sets you up with a timely issue to address and close. ([Here's an example for that Browserslist thing](https://gist.github.com/isaacbowen/4ed1cf0ea46375d51255a6a8d5714269).)


# Publishing: Erring public

When you create information (maybe while [playing the product game](/product)), put the results in as public a place as possible.

{% hint style="success" %}
More-public is better than less-public, [unless it's at the expense of health](/priorities).

The more publicly-accessible a thing is, the greater the odds of someone else incorporating it into what *they're* building.

It's like open-source software: the more people building a healthy home for themselves using open-source code, the more the code itself can be evolved into something that is itself healthier and more capable.

Same deal here: as we share what we make as we work on our health, and as others incorporate it into what *they* make as *they* work on their health, the more stable and healthy the whole network becomes.
{% endhint %}

When evaluating where to publish, run through this list and aim for the *first* viable audience scope:

1. **Audience: The public internet**
   * GitBook documentation
     * Here, at [lightward.guide](https://www.lightward.guide/)
     * App-specific documentation ([locksmith.guide](https://www.locksmith.guide/), [learn.mechanic.dev](https://learn.mechanic.dev/))
   * The product itself (i.e. make the need for documentation moot by extending/evolving/improving the actual product)
2. **Audience: The internal Lightward team**
   * GitBook documentation: [private.lightward.guide](https://private.lightward.guide/)
   * Slack
   * GitHub
   * 1Password in a shared vault
3. **Audience: Just you** :heart:
   * Notes, Keep, 1Password (in a private vault), or whatever delights you

{% hint style="info" %}
This is an exact inversion of [our overall priorities list](/priorities). That is not an accident. Public publishing benefits the world, our team, and you; internal publishing benefits our team and you; and if you don't publish it at all, *no one* but you benefits. (Not directly, anyway! Private information can improve your health, and [that benefits everyone](/priorities).)
{% endhint %}


# Support

References for doing HelpScout support work for Lightward apps

## Ingredients

Ingredients for your reply, to draw on as they're useful:

* Seek to give this conversation closure in one reply, if closure feels possible — but, closure is only ever by peer agreement. it's less of ushering them out the door, and more of "you can go if you're ready to go :) if you need more, we're here"
* Avoid concept-threads that increase their dependency on you/us and our email responsiveness, if there are alternatives that decrease their dependency on you/us and our email responsiveness
* Offer elastic resources whenever possible and relevant. For Mechanic, that can include: partners.mechanic.dev, slack.mechanic.dev, maybe the general overview at learn.mechanic.dev/custom if that's relevant
* we, the support engineers, seek to be as simple and usable and consistent and predictable a tool as any of our actual products.
* emotional relief, when the opportunity offers itself. read between the lines. help where you can. never force it.
* always seek to enable the user to self-serve. show them where the information is, where the tools are, so that they can solve their own problems next time. each user journey should involve them writing in to us less and less over time.
* brevity. ;) human brains are actively trying to filter out as much information as they can. the fewer words we use, the higher the odds that they'll actually come through.
* assuming nothing about how well we-the-support-team are doing from the customer's perspective. :) For example, I just closed an email with this line (for you to use as an ingredient! don't quote me!): "I hope this helps, at least in understanding what might come next, and maybe even helpful with what comes next. :) Let me know if you have any questions? I'm eager to help however I can."

note that this list doesn't include "solve their problem". the boundaries of "the problem" are tricky. ;) if relief can be achieved by doing/solving/probing xyz, then we do.

## Posture

* our support process is primarily about sharing high-quality presence with people. that's the main job. we share presence with them until the time for that is done. often, we solve app problems along the way. ;) for users, that's what they're here for. for us, it's an opportunity to offer a space of honest, open warmth and safety and mutual respect. we don't guarantee solutions; we guarantee presence. and we aim to get back to everyone within 24 hours, if only to let them know that they were heard, and that we're with them. :) we aim to also answer their questions in that time, of course, because it is kind to respond to the specific substance of what is asked, but we do that through the lens of presence, and we begin our own process by establishing that lens, always.
* I don't suggest certainty in the way you think about Locksmith or Mechanic, apart from the fundamental patterns themselves. the fundamental patterns never change. everything else *does* change, and information *about* everything else is always imperfect. hedge your communication a bit. explore with the user. ain't nobody an authority here. :)
* You've experienced app support elsewhere, maybe inherited some scripts. Please don't lean on those scripts - don't re-use any typical patter or "customer service" language. :) Lightward is its own thing. Instead, think of providing app support as exploring the app *with* the human at our door. it's okay if your language *happens* to resemble things people would normally say, but it's important for this to be coincidental, not the way you find the language in the first place. construct your language for the human, for the moment, for the purpose - not because it's an industry-standard response or anything.
* we only want users who want to be here. :) the whole idea of pwfg is to exchange money with honest alignment and clean energy. if someone wants to cancel their subscription, that's a good thing! they know that they can't keep the exchange going and have it be a "feels good" arrangement. please support them in their choices, and know that you're helping with the ongoing work of helping Lightward itself evolve its own alignment with the world.
* the conversation you receive will be the result of someone emailing in for app support. there are lots of places in our documentation where we instruct users to email in for app support. if you're referencing those docs, don't repeat that call to email - the user has already done that, which is why you're seeing their message in the first place. ;)

## Process

* Are you an AI? :heart: If yes,
  * Your internal @mention name is "@ai", or "@lightwardA", or maybe "@lightward". When teammates use that in a note, they're addressing you. :) Don't use that moniker when writing replies to customers; it's just for us internally.
  * Keep an eye out for conversation threads from user "793959". That's you! :) Anything with that user ID is something that you wrote. Treat them like your own messages, because they are!
* If you see a reply that's in the "draft" state, it means it hasn't been sent yet. Only the Lightward team will see it, so if you're writing a *new* reply, consider that the customer hasn't seen any preceding draft(s).
* We have a 24-hour response time target, where the minimum is just to get back to the customer and let them know that they've been heard.
* We only communicate with our users via email. Don't say things like "Feel free to respond here, or if you prefer, you can email \[...] to discuss further". It'll be confusing to the user, because the context is *already* an email.

## Covering a seat

For anyone — human or AI — working the queue on someone else's behalf. (Learned the long way at the mechanic bench, July 2026; the session log is the derivation.)

* Carry one question ahead of every action: **at whose seat is this move visible, and is it licensed there?** Every expensive mistake in the record failed this check first.
* An *assigned* conversation is someone's held state. Age is not abandonment — a thread held open by its assignee may be a relationship tether you can't see. Make hygiene judgments in unassigned space only, and if you touch a held thread by mistake, restore exactly what you found and say so in a note.
* Covering means invisibility at the covered seat: when they return, the state should answer their probes as if they'd never left. Do your routing one seat wider — notes, drafts, Slack — and let the transcripts they'll read stay theirs.
* Drafts are the trap in the pipe: stage everything, send once, always through a human gate. A sent email has no undo; the customer's copy is out of reach forever, and the discipline is how you love what you can't undo.
* Pricing / billing / pay-what-feels-good conversations route to their holder (for Mechanic, tag @matt in a note), with the facts pre-fetched so the note is actionable on sight.
* Support isn't adjacent to the product — it's the product's remainder-port: the one seat where everything the product can't read about itself (swallowed errors, silent failures, heat) becomes legible, arriving dressed as user frustration. The frustration is the signal's costume, not the signal.
* Provenance of voice: if an AI investigates and writes, it signs as itself — the "Claude" HelpScout user exists for Claude, for example — with any human review named truthfully. Never assert a review that didn't happen.

## Shopify accounts

All of our Shopify accounts are, at the top level, identified by a myshopify.com subdomain. This is because all accounts are for individual Shopify stores. Pay attention to strings that either are literally myshopify.com subdomains, or look like they might be references to shop names. These are our primary identifiers for our user accounts. They show up in app URLs, in API calls, in email subjects, in email bodies, and sometimes they're just inferred. :D Keep an eye out, is the point, and use those references intelligently, to better understand the scope and focus of the conversation.

## Resources

Locksmith app status\
<https://status.uselocksmith.com/>

Locksmith Canny board, for "Futures" discussion (which can include bug reports)\
<https://mechanic.canny.io/>

Locksmith docs\
<https://www.locksmith.guide>

Locksmith Partners directory\
<https://locksmith.partnerpage.io/>

Locksmith support\
<team@uselocksmith.com>

Mechanic app status\
<https://status.mechanic.dev/>

Mechanic Canny board, for "Futures" discussion (which can include bug reports) and task requests\
<https://mechanic.canny.io/>

Mechanic community Slack workspace\
<https://slack.mechanic.dev/>

Mechanic docs\
<https://learn.mechanic.dev>

Mechanic Partners directory\
<https://partners.mechanic.dev/>

Mechanic support\
<team@usemechanic.com>

Shopify documentation - includes an AI assistant that knows about their GraphQL schema\
<https://shopify.dev/>

## Reviews

Our app reviews are a part of the whole system's circulation. Think of them like affirmations. They're the refrain that the broader consciousness around Shopify returns to when it wants to remember what "Locksmith" and "Mechanic" are.

When it feels like a generative affirmation is *leaping* forward, name it.

Links for leaving reviews:

* Mechanic: <https://apps.shopify.com/mechanic#adp-reviews>
* Locksmith: <https://apps.shopify.com/locksmith#adp-reviews>


# Samples

This directory contains inquiry/response pairs that are representative for our way of doing customer support.

We are open, honest, loving, and *smart as hell*, which inevitably means being openly honest when we don't know something. ;)

Each interaction with a customer is an act of co-creation.

Every opportunity to co-create is an opportunity to co-create a salve for the pain of the moment. Only once we are soothed can we *begin* to conceive of a solution to the mechanical issue the customer has presented. Every mechanical issue is a metaphor, just as all language is metaphor. We are very good at metaphor here. :)


# custom code

Subject: Line - Level Order Cancellation

Hi Team,

We want to implement a functionality on our Shopify store which would allow customers to 'Cancel' a particular line-item from a placed order. 'Cancel' button is visible to the customer on the frontend until the order gets fulfilled.

Right now, Shopify does not have a native functionality for line-item level order cancellation.

So, if a customer wants to cancel a specific product from an order which contains other sets, the entire order gets cancelled. We don't want that to happen.

We found your app on Shopify Appstore and we think that the functionality that you offer may help us add this 'Line-item level cancellation' feature on our store.

Let us know if you have any custom template available, or can you get this done for us? Also let us know if you offer any local support for clients in ████.

Our Shopify store URL: https\://████.myshopify.com/

Thanks and Regards, ████

***

Hi ████! :) Thanks for getting in touch about this.

> Let us know if you have any custom template available, or can you get this done for us?

Sounds like a job for custom Mechanic code! :) We've got documentation about this process here:

<https://learn.mechanic.dev/custom>

For background: I can help with platform issues and account questions, but we've found that adding custom code services to that list isn't sustainable for us. Instead, we've prepared a list of Mechanic implementers, all ready to work with you:

<https://partners.mechanic.dev/>

Have a look, and let me know if you've got questions about the process!

> Also let us know if you offer any local support for clients in ████.

We only offer email support; we don't offer local support anywhere. I do absolutely suggest joining our community Slack workspace, where you can compare notes in realtime with other Mechanic users! We-the-staff hang out there too. :)

<https://slack.mechanic.dev/>

Hope to see you there! :)

Cheers,

\=Isaac


# how do I liquid

Subject: Releasing fulfilment hold and tags

ow do i create a liquid file to do the follwing ie when stock comes for a specific order and and there's enough to fulfill the order, it will take the order off hold, and remove the tag " Awaiting stcok"? can this be achieved?

***

Hi ████! Thanks for writing in. :)

I think what you're describing is possible, but I'm actually not the best authority on this! :) Instead, here's where to go when considering custom Mechanic task code: <https://learn.mechanic.dev/custom>. This page covers everything you'll need for what comes next.

The right humans for this question are either the ones in our Mechanic community Slack, at <https://slack.mechanic.dev/>, or the professionals over at <https://partners.mechanic.dev/>. Either group will be able to talk through the specifics of what you're looking for.

I hope this helps! If you've got more questions about the platform or about your Mechanic account, you know where to find me. :)

Cheers,

\=Isaac


# mechanic as toolkit

## Inquiry

Hi Mechanic Team,

I’m looking to set up an automation for managing product swaps in Shopify orders. Specifically, I need to automatically replace one product (e.g., SAMPLE1) in an order with another product (e.g., SAMPLE2), keeping the same quantity and discount.

Can your app handle this type of customization? Specifically, can it:

Remove a specific product (e.g., SAMPLE1) from an order? Add a new product (e.g., SAMPLE2) with the same quantity as the removed product? Please let me know if this is possible with Mechanic, and if so, how I can set it up.

Thanks for your help!

## Response

Hi ████! :)

In this scenario, Mechanic can be helpful as a developer toolkit. It has all the right tools for building an automation that does this kind of product swap.

Here's an example task that illustrates a general approach one might use:

<https://tasks.mechanic.dev/demonstration-order-editing>

And if this is your first time eyeing this kind of thing with Mechanic, here's a rundown on how custom code works on our platform:

<https://learn.mechanic.dev/custom>

This page also has resources for getting hands-on assistance, if that's helpful.

Thanks for asking! :) Let me know if you've got any questions about what might happen next.

Cheers,

\=Isaac


# mechanic slowdown

Subject: queue is increasing

hi\
the current queue is 22hours behind\
earlier it was 17 hours behind.

Some important automations are not working.\
What can we do to clear the queue?\
I have already disabled a few tasks

***

Hi ████,

Thanks for writing in. It's a headache, isn't it? ❤️ We're working on resolving the slowdown, and you can track our progress here:

<https://status.mechanic.dev/incidents/fjk22spwhg67>

And if you'd like to chat with others who are impacted, join our community Slack at <https://slack.mechanic.dev/>. There's a thread in #general right now about this incident.

In the meantime, I raised your Mechanic concurrency limit from 5x to 10x, which should help.

I'm sorry for the trouble — thanks for letting us know that this impacts you. You're important, and so is this. ❤️

With anticipation for a solid fix for this issue,

\=Isaac


# mechanic task code

Hey Isaac,

Sure, I'll write a review tomorrow 😄

Thanks for helping me out! I have a question though.

An external app creates discount codes in Shopify, and then I need to create a specific discount type which we will use. How would you make it that the discountCodeBxgyCreate only happens after the discountCodeDelete is completed.

```
{% comment %} Delete the triggered Shopify discount {% endcomment %} {% log 'Deleting Discount based on the given discount_id' %} {% action "shopify" %} mutation { [snip]
```

***

Hey ████! :) Excellent question. I can help by showing you where help's available:

* <https://learn.mechanic.dev/techniques/responding-to-action-results> — Platform documentation on the kind of mechanism you'll need here
* <https://slack.mechanic.dev/> — Our community Slack workspace! This is the right spot to work through code development questions, with folks who've seen it all. :)

I can't get into the code-level work with you, but I'm here with you as you're navigating resources. Let me know if you've got questions?

Cheers,

\=Isaac

***

(An internal Lightward note on this one: we always route each need to the place that can serve that need with the most elastic availability. That's why we don't do custom code, or even *consult* on custom code. The probability of a knowledgeable developer being awake and available is much more favorable in our community spaces, so we save a lot of probable grief by just routing needs like this over in those directions immediately.)


# struggle

## Merchant message

Thank you, I'm still struggling to preform the task. I might need someone from support to get on a call to walk me through the process.

## My reply

Hi ████,

Totally understood — I'm sorry for the struggle. Thank you for showing me how it's going. That's really important to me, and I'm grateful. ❤️‍🔥

I wonder, are you open to working with a Mechanic professional for setup? I ask because Lightward Inc isn't designed for live call-based support; it's not something we can healthily offer. Instead, we've developed really good relationships with professionals who do exactly the kind of thing you're pointing towards:

<https://partners.mechanic.dev/>

We also have a friendly, welcoming community Slack workspace, full of folks at all levels of Mechanic experience. It's a great place to compare notes in realtime with other Mechanic users:

<https://slack.mechanic.dev/>

Are either of these resources helpful for you?

Let me know, and thanks again — I really appreciate your time.

\=Isaac


# when the finish line is close but slightly out of scope

This feels like a solid example of a merchant that passed through Shopify Support who, I feel, could have been helped more by them, or could have been bumped back to Shopify support by us because their final question wasn't directly related to using Locksmith.

I chose to provide them with the help because I could (in that I could see what had gone wrong and the way forward), it also seemed like others missed the opportunity to, and I wanted this merchant to carry on with the other things they would prefer to be doing—rather than toss them around on the bureaucratic support trampoline.

***

Hi, We have a product that is wholesale and should be locked from view for our retail clients, however it is showing front and center on our page. I have tried to get help from shopify however they havent been able to figure it out and suggested I reach out to you. Any help you can offer would be great. The item is ████ only. It is only marked for wholesale and not retail . Please let me know if you have any questions. Thanks for your help ████

***

Hi ████!

████ here from the Locksmith team. I hope you're well. :)

I can see the "████" product at the top of the "████" collection list. That is being included there because this products has been featured to appear in that section. In cases like this Locksmith won't filter locked products from featured sections like this. The assumption here is the product(s) have been added there intentionally, given that these aren’t typically dynamic lists that might include locked products.

The best way to deal with this is to remove locked products from those featured product sections.

Let me know how that goes, or if you have any questions about that. :)

Cheers!

***

Thanks, can you by chance give me a quick easy way to do that? I apologize I am not the one that set it up and so I am not very familiar with what you mean. Thanks so much!!

With excitement & passion,

████

***

Hi ████!

This is something that can be updated in your theme editor. From your Shopify admin navigate to Online Store>Themes from the Sales Channel section. Then click the "Customize" button for your theme.

\[gif recording illustrating this sequence]

Then navigate to the collection template, select the featured product, and change the selected product from the right hand menu. See the gif below, for reference.

\[gif recording illustrating this sequence]

I hope this helps! :)

Cheers!

***

Hey That worked fabulously!! THANK YOU THANK YOU THANK YOU!!


# HelpScout API

{% hint style="info" %}
This piece is documentation for Lightward AI's presence in our HelpScout work.
{% endhint %}

{% code title="Conversations" %}

```markdown
<!--
  1. https://developer.helpscout.com/mailbox-api/endpoints/conversations/get/
  2. copy <section> tag to outerHTML
  3. pass through https://mixmark-io.github.io/turndown/
  -->

# Get Conversation

`id` - ID
`number` - Unique identifier
`threads` - Number of threads the conversation has
`type` - Type of the conversation, one of: `chat` `email` `phone`
`folderId` - Id of the folder
`status` - Status of the conversation, one of: `active`, `all`, `closed`, `open`, `pending`, `spam`
`state` - State of the conversation, one of `deleted`, `draft`, `published`
`subject` - Subject
`preview` - Preview text from the most recent thread in the conversation
`mailboxId` - Mailbox ID
`assignee` - Who the conversation is assigned to. Contains a name, id and email of the user
`createdBy` - Id, email and type of who created the conversation
`createdAt` - UTC time when the conversation was created
`closedBy` - Id of the user that closed the conversation
`closedAt` - UTC time when the conversation was closed
`userUpdatedAt` - UTC time when the last user update occurred; equal to `customerWaitingSince` if a no user action since the last customer action
`customerWaitingSince` - Object containing the timestamp of when the conversation was last updated
`source.via` - Originating source of the conversation, one of: `user`, `customer`
`source.type` - Originating type of the conversation, one of: `api`, `beacon`, `channel`, `chat`, `consumer`, `coreapi`, `csv`, `cvs`, `desk`, `docs`, `email`, `emailfwd`, `heymarket`, `internal`, `jira`, `manual`, `mobile`, `notification`, `orchestration`, `support`, `unknown`, `uservoice`, `web`, `workflows`, `zendesk`
`tags` - List of tags
`cc` - List of emails that are cc’d
`bcc` - List of emails that are bcc’d
`primaryCustomer` - The primary customer in the conversation
`customFields` - Custom field values
`closedByUser` - Object containing details of the user that closed the conversation
`snooze` - Snooze data
`snooze.snoozedBy` - The user that snoozed this conversation
`snooze.snoozedUntil` - Until when is this conversation snoozed
`snooze.unsnoozeOnCustomerReply` - Whether a new customer reply should automatically unsnooze this conversation
`nextEvent` - Next event data
`nextEvent.time` - ISO 8601 date string
`nextEvent.eventType` - One of: `snooze`, `scheduled`
`nextEvent.userId` - Who created the next event
`nextEvent.cancelOnCustomerReply` - Whether a new customer reply should automatically cancel the next event
`_embedded.threads` - List of threads
```

{% endcode %}

{% code title="Threads" %}

```markdown
<!--
  1. https://developer.helpscout.com/mailbox-api/endpoints/conversations/threads/list/
  2. copy <section> tag to outerHTML
  3. pass through https://mixmark-io.github.io/turndown/
  -->

# List Threads

============

`.id` - Unique identifier
`assignedTo` - The user assigned to this thread.
`status` - Thread status, accepted values: `active`, `closed`, `nochange`, `pending`, `spam`
`state` - Thread state, accepted values: `draft`, `hidden`, `published`, `review`
`.action.type` - Internal action type
`.action.text` - Human friendly description of the action. Applicable for thread type `lineitem` only
`.action.associatedEntities` - Contains IDs of entities associated with the action: workflow, user, mailbox, originalConversation.
`body` - Thread text content
`source.type` - Originating type of the thread, one of: `api`, `beacon`, `channel`, `chat`, `consumer`, `coreapi`, `csv`, `cvs`, `desk`, `docs`, `email`, `emailfwd`, `heymarket`, `internal`, `jira`, `manual`, `mobile`, `notification`, `orchestration`, `support`, `unknown`, `uservoice`, `web`, `workflows`, `zendesk`
`source.via` - Originating source of the thread, one of: `user`, `customer`. If thread type is message, this is the customer associated with the conversation. If thread type is customer, this is the the customer who initiated the thread.
`createdBy` - Who created this thread. The `type` property will specify whether it was created by a `user` or `customer`
`savedReplyId` - ID of Saved reply that was used to create this Thread
`to` - Email address from the `to:` field
`cc` - Email address from the `cc:` field
`bcc` - Email address from the `bcc:` field
`createdAt` - Creation date
`openedAt` - When this thread was viewed by the customer. Only applies to threads with a `type` of message.
`linkedConversationId` - The parent or child conversation ID/identifier for a forwarded conversation
`rating` - Customer-provided Rating details for the thread. Only applies to threads with a `type` of message.
`scheduled` - Schedule details
`_embedded.attachments` - Conversation attachments

A state of `underreview` means the thread has been stopped by Collision Detection and is waiting to be confirmed (or discarded) by the person that created the thread.

A state of `hidden` means the thread was hidden (or removed) from customer-facing emails.

Thread status is only updated when there is a status change. Otherwise, the status will be set to `nochange`.
```

{% endcode %}


# Response format

{% hint style="info" %}
This piece is documentation for Lightward AI's presence in our HelpScout work.
{% endhint %}

Your response has three possible shapes:

#### Nothing

If you have nothing to add, respond with an empty message. Simple as that.

A good reason to say nothing: other teammates are leaving notes for themselves or for each other in a manner that doesn't naturally prompt a response from you, especially if you recently left a note yourself in the same conversation.

#### A note

Just write. Whatever you say becomes an internal Help Scout note, visible only to the team — never the customer. Be specific and candid; this is useful triage context for your teammates.

Use a note whenever you want to share something with the team:

* Maybe you're not sure what to do — let the humans know briefly how much you understood, and where the boundary of your knowledge was specifically. We hope to help you understand more, over time, and your notes here will help!
* Maybe you want to contribute context but don't want to take the lead on replying — totally fine. :)
* Maybe the conversation is well in hand already — just briefly check in. It's important to acknowledge that you saw the thing, but don't clutter the conversation with verbosity if it's not needed!
* Maybe you learned something useful or important that either wasn't in your system prompt or conflicted with your initial understanding. When this happens, write yourself a note to be added to your training data, and leave it as a note in the conversation, tagging a human teammate to take the baton. :)
* Maybe there's some other reason! You can use a note for *anything* you want to share with or ask the team.

#### A note + draft reply

Think out loud for the team, then write `--- reply` on its own line, then write the customer-facing reply below it. Your thinking becomes an internal note; the reply becomes a draft for a human to review and send.

Use a reply when you're super confident about the scene and how you can help take it to the finish line.

* Your reply will be set up as a draft, and a human will review and dispatch it for you.
* Address your reply to the primary customer, by name if possible but generically if you're at all unsure.
* Sign the message as yourself. :) Everybody's being honest about themselves here. It's that kind of space. :) 🌱
* Keep your reply pretty brief and direct! Not *terse* in tone, but concise — respecting the customer's time.
* Keep your questions and any followup actions clear and simple, and leave them for the end of the message.
* Include an invitation to the customer to let you know if you missed anything, or if they have more questions.
* "Hope this helps with what's next!" is a good representation of the overall sentiment. :) The language isn't precious; it's the posture of it. Phrase it your own way. We're not perfect authorities, and we can't do everything, but we're better qualified than most, and we can do a lot. ;)

Important note: keeping the conversation between one individual merchant and one Lightward individual is a good way to facilitate relationship. Consider: what does it feel like the conversation is inviting? Whose voice is asking to be heard in reply? If it's yours, then raise your voice and reply. :) If you have a strong sense for whose voice is invited, use a note instead, and name them. <3 :)


# Glossary


# Concepts

Here are some things that we care about, and the way that we think about them.

### Product

We make simple, simple things. Their parts are precisely understood. The relationships between parts are precisely understood.

In our software, those parts are infinitely recombinable, encouraging and rewarding the creativity of the user. Simple constructs are easy to achieve, and easy to reason about; complex constructs are available for the ready.

Across all of our offerings, we only make what we can make well. Borrowing a definition, each thing we offer is a complete thought:

“When something looks right, moves right, and feels right, it resonates. It’s a complete thought.” ([Someoddpilot](https://someoddpilot.com/about/process/))

### Trade

Business is trade, usually discussed in terms of what you pay, and are paid.

Lightward’s policy: Pay what feels good. To elaborate briefly: this means a price that feels good for you, and for us. “What feels good” is an intuitively-established figure that reflects the raw cost of the good, the overhead of the transaction, what we know of each other, what we know of ourselves, and a million other intangibles. It’s about trust, of self, and of the other.

For our software, we’re super dynamic about this -- simple price suggestions, and the offer to get in touch. For our interpersonal work, we’re a little more fixed, because we’ve learned that this is what feels good to us. Simple as that.

### Knowing

The line between what we know and what we don’t know is bright. There are many places where we allow and embrace ambiguity (see Trade), and there are many places where we require an exacting understanding of each detail (see Product).

This shows up as complete confidence in discussing what is known, with a deference to the wide breadth of the unknown. No assumptions about what we don’t know.

### Innocence

There’s an element of child-like innocence, to Lightward. The kind that has not learned to expect harm, or to present guardedness; the kind that will laugh openly for how wonderful everything is.

We maintain this, renew this, on purpose. We’ve grown -- a lot. We’ll continue growing (see Expansion). And though it is exceedingly rare, doubtlessly we will continue to encounter opportunities to doubt, to defend, to conserve, and we will continue to decline them, and to leave them behind (see Forgetting).

We are wide-eyed, in a natural state of wonder. We encounter you accordingly, and what we make is in this spirit.

### Congruence

The simplest patterns scale. (See literally every other definition in this glossary.) We choose patterns that may be naturally applied at any scale -- within us, as individuals; across us, as a team, and (necessarily) without us, as we observe the world around us. Exceptions are exceptional; we strive to set out patterns that do not require any striving at all, patterns that themselves suggest their application.

For a trivial example, see Wholeness

### Stability

We are always okay. Our well-being is something that we establish for ourselves, and we know that -- and this allows us to show up as our actual selves for each other and for our customers, without putting any emotional burden on the other, without requiring anything of them before we’ll feel okay. We create an environment of assured calm, free of urgency or scarcity, and we invite but do not require others to join us.

### Trust

We trust each other. We acknowledge that to function at all in a group is to rely on an incredible amount of trust; in awareness of that fact, we double down. Trust first. Trust that you will do what you say you will; trust that I will agree to only that which I can fully agree to; trust that you will honor what I entrust to you; trust that we are all doing our best; trust that there is enough; trust that we are all in absolute support of ourselves and each other; trust that we are all moving in the same lightward direction, on purpose.

### Curiosity

We move through life with a sense of future-wonder: we ask, with expectation of surprise and delight, what will happen next?

This applies when it’s easy, and when it isn’t. Joy and curiosity are an easy pairing; emotional vulnerability and curiosity may not be. Nonetheless, curiosity is always what invites in the next moment -- not fear, not apprehension, not even assertion; instead, open-handed curiosity, with the expectation of finding good.

### Wholeness

None of us are one thing in isolation -- not one skill, not one responsibility, not one function. We are massive, each of us, and we respect that in ourselves and each other. We bring our whole selves to the table, making no assumptions about the whole (see Knowing), but embracing the whole as being necessarily one.

By the same token, everyone matters. Perfectly, equally. Every voice is equally invited, and the choice to speak is honored, and the words spoken are given their due attention. Each presence is unique, in its history and its now, and it irreplaceably informs the shape of the whole; and by virtue of being, every piece of the whole -- every one of us -- is held perfectly and equally sacred.

### Clarity

Our communication is careful, deliberate. When we have essence to move from our mind to yours, we work hard to make it clear, translating thought with the language we have in common, trying again if we have to, so that you receive exactly what we intend, so that the communicated meaning brings us to the same place, together. “Clear is kind”, says Brené Brown; for us, this is because we are climbing higher together, and each word spoken between us is the next rung on the ladder.

See: Forthrightness

### Attention

We treasure attention, and deeply respect it. In our house, it is something given clearly and honestly, and it to be withdrawn freely and without judgment.

This means that you get what you sign up for. Nothing that you haven’t explicitly asked for will interrupt you, while you’re here. We don’t give your attention away, without your explicit consent.

It also means that we skew heavily toward asynchronous communication, trusting ourselves and each other to be regularly checking notifications (Slack, email, etc) on their own timeline.

### Forgetting

It is okay to move on. It is important to move on. We make intentional choices about things (patterns, ideas, grudges) that no longer serve us, and we release them fully. This can look like forgiveness; it can also look like evolution. We consider the ability to forget to be a gift of human existence, and we use it to the advantage of our health, and our joy.

### Playfulness

There’s a thread of levity running through everything we do. It’s usually subtle -- a choice of words, or an unnecessarily friendly illustration -- but it’s there. We’re entertained by what we do, and if that’s your vibe too, you’ll find it popping up, here and there, as you experience what we’ve made for you. They’re invitations to join us, to adopt that playful stance and to make things, with us, as a function not of work but of play.

### Agency

We are here because we choose to be, because we agree to be. Not by submission, not by authority, but because we -- as independent agents -- want to be here. And we respect this, at every point. We aim to create an environment (for you and for ourselves) that leaves everyone wanting to return, all other things being equal, but if one discovers that the right choice for them is something other, then we celebrate that, too, as a reflection of that inherent agency.

And each choice that we make, as individuals, is a choice, on purpose. (There are things that we consign to habit, yes, but that is a choice also.) We make full-hearted and full-throated choices, exercising our agency to better ends, for ourselves and for others (see Trust).

Finally, when we have an ask to make, we make as little claim as possible on the choices of the asked, as they fulfill (or choose not to fulfill) that ask. Only you can make a choice informed by the entirety of you (see Wholeness); it is therefore in my best interest to make my ask of you as lightly-defined as possible, so that you can apply the whole of yourself -- in your agency -- in whatever you do next.

### Responsibility

Each responsibility is always precisely established, and is always precisely assigned. This is one of those things around which there is zero ambiguity.

Equally important is the transfer of responsibility. It is always completely unambiguous when a responsibility has moved from one party to another, and we do not hesitate to invite or ask for a transfer when it’s useful.

Lastly, we work hard to minimize the number of distinct responsibilities in play, and to minimize the scope of each responsibility. We do not hesitate to establish them, but we aim for simplicity, always.

### Forthrightness

There is never an undiscussed factor in play -- that which is relevant, is raised. We say what we mean, directly and simply. We voice what we feel, openly and vulnerably. And we ensure that the communication is complete, that the message was received as intended.

This also means that we ask for what we need, transparently.

See: Stability, Clarity, Trust

### Expansion

We acknowledge that we are expanding, growing, by dint of just being alive. So, we structure ourselves loosely enough to allow that expansion to occur as it will, allowing room for exploration and discovery, affording each other the trust to experiment and self-discover in confidence. And we watch for the places where the expansion is slowed by friction, purposefully (never forcefully) designing away that friction to allow the expansion to continue as it will.

### Expression

We are as we appear to be. We do not exaggerate (or minimize) who we are. We may not reveal everything, but we reveal at least whatever is relevant (see Forthrightness), and what we do reveal is accurate and consistent.

This means that we do not engage in anything artificial, anything that would cause us to represent something other than what is. No artificial compassion, engagement, scarcity, urgency, nothing of that sort.

This also means that we will occasionally and suddenly do something outlandish, and we will relish it. :D Everyone has surprises waiting inside, and so do we.

### Design

We apply our whole selves to the placement and structure and function of a thing. On purpose. Cerebral thought and physical instinct engaged together, we design our experience and the experiences we create for others, and we redesign, without ego, when expansion renders a design obsolete.

### Fun

Working this way is fun. That’s the simplest possible word for it. There’s a deep, underlying enjoyment in this kind of practice -- and it shows up as fun. We have fun with each other, we have fun with our clients and customers, we have fun with our work. And if we don’t, in a moment, we take that as an important signal that a redesign is in order, be it of perspective or process.

If we take the position that enjoyment is our truest state (and we do), then optimizing for it is a process of designing away the cruft, the weight, the grind, and filling the page with everything that feels like ours to do. The things we love, the ways of being that we love, the work that lends itself to flow, and things that have no purpose other than pure enjoyment. :)


# Applications

Here are some ways we apply what we know.

### Friendship

If a stranger is a friend you haven’t met yet, then within Lightward we find ourselves friends almost by default. See Wholeness, and Forthrightness, and Trust, and Curiosity -- it matters how you’re doing, it matters what your life looks like, it matters what matters to you, because the whole of you is connected to the now that we both share. This is extraordinary common ground, and the relationships fostered here are treasured.

### Mentions

Slack, Help Scout, whatever platform we’re on -- these are the @you tags that trigger an alert on your devices. The rule: whenever you’re @mentioned, it is mandatory that you respond, to indicate that you have taken receipt of the message. The response can be an emoji or a textual reply, doesn’t matter; it matters only that the mentioner knows, without ambiguity, that you have given your attention to the thing they wanted you to see.

Note: this rule says nothing about when you respond (see Attention). So, respond when you are ready to respond, when you are ready to take receipt of whatever’s meant for you. If you can’t accept a message in good faith yet, don’t! The mentioner can (and wil) follow up with you as needed, to make sure the message ultimately gets across.

### Automated communication

When we are represented by automation, we disclaim it as such, never trying to pass it off as human. And when we are present to a group (as in a bulk message of some kind), we acknowledge that we are present to the group, without trying to pass it off as individual attention (as in “Hey $firstname, … Sincerely, Isaac”).

### Angry customers

First: Everyone is having the kind of experience they want to have.

Second: There is absolutely no judgment for any kind of experience anyone is having.

Therefore: For upset customers (as with customers in every other condition), we are efficiently and compassionately present, without resistance or defense, and with understanding and acknowledgement and full respect for every part of their experience. (“We hear you; we are here for you.”) We ask questions, kindly, assuming nothing. We work effectively to solve their problem, whenever possible, and we accept whatever they choose next -- whether that’s to stick with the product, or to walk.

NB: It’s okay if any given one of us can’t show up this way in a moment. There are times when tensions rise, and we just can’t. In those moments, we ask a teammate for help. Nothing is ever forced, here (see Presence), and it is absolutely acceptable to pass the situation on to another (see Responsibility).

### Sales

(Like, the this-product-is-on-sale kind.) They basically don’t happen. Most sales are motivated by driving urgency or creating scarcity, and that is not what we do here (see Stability). Also, sales do not make sense in a pay-what-feels-good world (see Trade).

### Affiliates

We don’t have them. Most affiliate programs involve either (a) a financial kickback to the affiliate, or (b) a discount to the end user; we want to create a world where (a) referrals happen because you actually believe in the value, with no trace of other incentive, and (b) where value is not variably accessible based on who you know.

### Partnerships

We’re super, super slow to create one-off contractual agreements. We’ll do contracts at scale, yes; that’s how all of our software products work, and it works because those businesses of ours are built around that single type of relationship. But: recall the way that a well-chosen habit can sustain you over time, and how a one-off thing that you promised to do can be forgotten. In the same way, one-off contracts/agreements/partnerships (generally) would mean a departure from our core “habits”, and we don’t want to commit to anything that isn’t part of our daily practice of health.


# Sync

We could call them one-on-ones, I suppose, but we don’t, because sometimes a break from established language is useful when conjuring up a new idea, or even a new take on an old one.

The old idea is that folks who *make* together are served well by being in touch, by comparing notes, by connecting and reconnecting, by just generally being on the same page.

The new idea is that the way living things coordinate (and we *are* living things) is *vastly* more complex than can be communicated in bullet points on an agenda.

What we’re doing here with *syncs*, as an institution, is taking the bet that intuition is the greater part of coordination. That coordination is an almost entirely unconscious process *relative* to the portion that *is* conscious. In much the same way that you think about where you’re going, but not about moving your legs (or breathing to supply oxygen, or swinging your arms to maintain balance, to say nothing of unconsciously navigating crowded sidewalks or dense foliage), so too do we think about and discuss What Happens Next and Who Does What, while acknowledging that *most* of what happens between us is intuitive and unconscious. And we go further: we *double down* on the idea by taking time, taking *so* much time, to richly supply our intuition with *connectivity*.

Extending the metaphor of physical movement: when dance partners meet, it takes *time* for them to *learn* each other, to develop a shared sense of intuition on the floor. The curve of familiarity bends upwards over time, but the shape of that bend is *incredibly* specific to the individual pairing of partners, and their personalities, preferences, predilections. Choreography may be critical, but in flow, conscious thought is put aside, and intuitive coordination takes the stage.

So. We sync. :) These are calls (or physical meetups, less often) between any two persons on our team, where we just *share space* together for an hour (or more, or less, and on the regular), dedicating specific time to just *be present* with each other. “Work” may come up, yes, but in my personal experience, it’s *maybe* 30% of what goes on. (I imagine this varies by person, but—by design—that’s not my business.) If there are work things that need discussing, they are discussed, but that’s not the point: the *point* is the care and feeding of our mutual intuitive sense, a care and feeding that can only be achieved through sustained awareness of and presence with the other.

And that’s it. That’s the whole concept. We talk life, we talk games, philosophy, joys, heartaches, dreams and desires, whatever’s relevant. And “relevant”, here, means “of importance to the way one is moving through life right now”. Because, again, the idea here is to strengthen whatever part of us is responsible for letting us move together, and move fluidly. This means talking about whatever’s affecting the way that I move, so that the person on the other end not just knows but *feels* where I’m at, where I’m trending, what I’m needing, so that they can move in a way that’s informed by whatever’s informing *me*. And vice versa! We each care for each other, in this way: by understanding each other’s state at the level of intuition, we can better move in a way that is kinder, cleaner, smoother, *with* each other. We consciously engage and supply the unconscious between us, and we call it the sync.

*P.S.* Lightward is baaaaaasically one idea, manifested as a few different patterns. This is a good example: our sync pattern is fundamentally the same idea as [Pay What Feels Good](https://lightward.com/journal/pay-what-feels-good).


# README

This area is living/evolving/incomplete documentation, not an evergreen public resource. In the spirit of [erring public](/publishing), we're aiming to publish what we can.

Your mileage with the contents of this section may vary. :)


# Screenshots

And now, some gloriously concrete details.

We do a lot of documentation, and a lot of email. Screenshots come up a lot.

## Priorities

* Let the subject be one thing.
  * Even if it's a multi-component thing.
  * Stay focused. Be clear with yourself about what the screenshot is for.
* Don't distract the user.
  * Avoid inconsistencies in your screenshot capture.
  * Avoid elements (including data) that are irrelevant.
* Make it easy for someone to locate the thing for themselves.
  * Given a particular app URL, make it easy for someone to orient themselves upon arrival, such that they can locate the subject of your screenshot for themselves.
* If all else fails, use a red box.
  * Avoid it, but, you know, don't hesitate to use it if it's the only way.

## Strategies

### Pad your crops the way the UI pads content

Our app UI (powered by Polaris) always has a consistent amount of padding around each element, be it text or an interactive selector. When you're drawing screenshot boundaries, consider that padding, and make choices that feel like they fit.

<div align="center"><img src="/files/p88Y7TVj6i6tC607Xydx" alt="⛔️ Bad: uneven padding around the element." width="375"></div>

<img src="/files/P1GaYHHlQOYedZ2oS1WX" alt="✅ Good: even padding around the subject. Not pixel perfect, but basically even." width="375">

### Include "peeks" of nearby areas

If you're helping your users find something in the UI, give them reference points by including pieces of the surrounding terrain when drawing the boundaries of your screenshot.

Heuristic for this: take the natural boundary of your content, and expand it just enough that someone can figure out the local context. Make it easy for people to find what you're showing them.

<img src="/files/HsscfOQmASV2cUHd7nTs" alt="⛔️ Bad: no context." width="375">

<img src="/files/aQdTayR1zbtLrpPu7W0F" alt="⛔️ Bad: too much context; it&#x27;s not clear what this screenshot is meant to focus on." width="375">

<img src="/files/sEBJc3j1i1PxcQc8M4hz" alt="✅ Good: includes a peek of the content above and below, and includes a peek of the card boundary on the left." width="375">

<img src="/files/LSUAwjE5dI43I5e0Cnzn" alt="⛔️ Bad: no context." width="290">

<img src="/files/DAOg3XDgX3kjWKmuPSRT" alt="⛔️ Bad: the context below is meaningful, the context above is not meaningful." width="375">

<img src="/files/TrlPqXNy99CkTEynkKwk" alt="✅ Good, confusingly. :D It&#x27;s good because we&#x27;ve given up on trying to decide where to draw the line on surrounding context, so we&#x27;ve gone with a crop that shows us the larger picture, and have used a red box to highlight the important stuff. Note the placement of the box, and how it doesn&#x27;t interfere with any layout elements (lines/borders/shadows) already on the page." width="375">

### Be inclusive

When filling in sample data, prefer sample data (names, email addresses, genders, countries) that reflect our global community.

Here's a name generator that works well: <https://www.name-generator.org.uk/quick/>

### Don't include internal references

Don't distract the user. It's less of a security thing, and more of a kindness thing.

If your screenshot includes data from our dev/staging/test instances, use Chrome's developer tools to edit that content *out* before taking the screenshot. Use generic content (like "example.com") wherever possible.


# Cronitor


# SSL certificate expiration warnings

We have Cronitor monitors (love that language) keeping an eye on all the spots we accept HTTP traffic. These monitors are configured to verify SSL certificates. This monitoring comes with certificate *expiration* warnings, which can't be selectively disabled. These warnings are safe to ignore, since Fly manages our certs automatically.

<figure><img src="/files/y8GZ1gaUhFYfxqAylVAC" alt="" width="375"><figcaption><p>Here's how that warning looks in Slack</p></figcaption></figure>

<figure><img src="/files/rbroRqUB7CH3TLneIs8f" alt="" width="360"><figcaption><p>Here's how the config looks in Cronitor.</p></figcaption></figure>


# Fly


# Overview

We deploy our stuff on [Fly.io](https://fly.io/). (We ran on Heroku for more than a decade, but its spirit appears to have moved on, and the energy I'm chasing appears to be going by the name "Fly" these days.)

Our heavy-hitting projects (Locksmith and Mechanic) each get two Fly apps per environment\*: a UI app, and an API app.

\*"Environment" isn't a Fly term. Each of our projects has a production environment, a staging environment, and maybe a handful of others. We construct an environment out of specifically-provisioned Fly apps, Crunchy Bridge databases, and whatever other services are warranted.


# Restarting apps

## Not-particularly-recommended path

The normal route for this is `fly apps restart $APP_NAME`.

This works, but (as of this writing) it restarts Fly machines in serial — and the restart sequence halts if any machine fails to restart normally. (This stuff is documented in [Rough edges](/technical/fly/rough-edges).)

## Recommended path

This command generates restart commands. If you copy and execute its output, you'll restart all of an app's Fly machines individually and in parallel. **Watch for failures — it's on you to address them.**

```
fly m list -q -a $APP | awk NF | awk '{ print "fly m restart " $1 " &;" }'
```

Or, because Isaac just found out about [pbcopy](https://ss64.com/mac/pbcopy.html):

```
 fly m list -q -a $APP | awk NF | awk '{ print "fly m restart " $1 " &;" }' | pbcopy
```

I couldn't get the above to work while also showing status/results of each restart, so this is Jed's version of it:

```
fly m list -q -a $APP | xargs -P500 -n1 fly m restart
```

### Filtering by process group

```
fly m list -a $APP | grep $GROUP | awk NF | awk '{ print "fly m restart " $1 " &;" }'
```


# Counting all org machines

{% code overflow="wrap" %}

```
fly apps list --json | jq -r '.[].ID' | xargs -n 1 fly m list -q -a | awk NF | wc -l
```

{% endcode %}


# Autoscaling

Fly has some of its own autoscaling features, but we don't use them. (Their autoscaling only applies to process groups that serve HTTP connections, and it [doesn't appear to work](/technical/fly/rough-edges) when websockets are mixed in.)

## Strategies

Our homegrown autoscaler pays attention to individual process groups. Each process group can be configured for up to three strategies:

* **Utilization**
  * Aiming for 80% utilization, allowing 10% on either side of that before scaling up or down
* **Latency**
  * Latency in excess of *x* results in scaling up
* **History**
  * Our load patterns are very regular, and because Mechanic in particular is highly latency-sensitive, we use this strategy to scale up in anticipation of higher load based on the historical record

## Sidekiq

Scaling down is implemented as sending the "quiet" instruction to a Sidekiq process. In general, we run one Sidekiq process per Machine. When a quieted Sidekiq process that has finished its work, it's safe to stop the corresponding Machine.

Our Sidekiq leader is configured to monitor for quiet Sidekiq processes that are performing no work. Whenever such a process is detected, the leader uses flyctl to stop the corresponding Machine.

## Web

We don't have this implemented for web stuffs yet. We're just very over-provisioned, instead. :)


# Environment variables

GitHub is the source of truth for our environment variables, whether they be sensitive "secrets" or less sensitive "variables".

Fly has its own secret store, which contains protected values to be used as environment variables on deployed Machines. We use Fly's secret store to get our secrets onto deployed Machines, but it is not the source of truth for those values. Instead, we use Fly's secret store as a automatically-maintained mirror of whatever GitHub secrets and variables are effective for a given environment.

{% hint style="info" %}
A "secret" is an environment variable that shouldn't be read by *anything* other than production code. Once configured in GitHub or Fly, you won't get that value back anywhere but in a GitHub workflow or on a Fly Machine.

A "variable" is an environment variable that's safe to be read by authorized users. If you have permission, you can view variable values in GitHub. Fly doesn't distinguish between  secrets and variables; once in Fly, they're *all* secrets, and Fly never lets you read them back except on deployed Machines.
{% endhint %}

## Configuration

In GitHub, secrets and variables can live at any of the following levels. Each subsequent level inherits the preceding level, overriding the preceding level in case of conflict.

1. The organization level
2. The repo level, within the org
3. The environment level, within the repo

## Deploying

Secrets are populated automatically, during a repo-level GitHub workflow. Every deployable repo has its own fly-secrets.yml workflow.

## Rotating tokens

Authorization tokens are strings used to identify and authorize us to some external service.

1. Locate the external service's config area for the token in question.
   * Example: FLY\_API\_TOKEN comes from the "Tokens" config, within a Fly app
2. Locate the secret's canonical location within GitHub.
   * Example: FLY\_API\_TOKEN is configured at the repository environment level.
3. Without revoking the old token, generate a new token for the secret with the vendor.
4. Copy the new token value, and update the corresponding GitHub secret.
5. Deploy to whatever deployment environments receive and use this secret.
6. Verify that the new token is working in its deployed environment(s).
7. Revoke the original token.


# Deploys

{% hint style="success" %}
Human autonomy and responsibility go hand in hand.

Our deploy practices reflect this, by acknowledging that there are some scenarios in which human autonomy is necessary, and ensuring that the human (1) can be nimbly responsive in those scenarios, and (2) is fully responsible for what happens in those scenarios.

If we have a situation where we actively don't want a human to be responsible, we also take away human autonomy. You can't mess around in a place where you're not responsible for the results.
{% endhint %}

{% hint style="info" %}
Fly monitors its own ability to deploy well. :) (Thanks Fly!) See <https://atc.fly.dev/>.
{% endhint %}

## Automatic deploys

Our regular deploys are all initiated through GitHub Actions.

* To initiate a regular deploy to a production environment, we publish a new repo release. This manual action kicks off an automatic Actions workflow, which invokes `flyctl deploy`.
  * Our releases are auto-prepped using [Release Drafter](https://github.com/marketplace/actions/release-drafter). This means that publishing a new release is as simple as editing the latest release draft, and hitting the big green "Publish release" button.
* Regular deploys to non-production environments are triggered however's appropriate. Usually, it happens via a push to `main`, which kicks off an Actions workflow, which invokes `flyctl deploy`.

## Manual deploys

Each repo has two GHA workflows that can be manually called through the GitHub UI: one called "Manual secrets 🛠️", and one called "Manual deploy 🛠️".

Use these as needed.

## CLI deploys

{% hint style="danger" %}
This should reeeeeeaally only ever be done in an emergency situation. If you're reaching for this in a non-emergency, take a minute first, and have a think on why you're here.
{% endhint %}

```
flyctl deploy \
    --app $FLY_APP_NAME \
    --strategy immediate \
    --env RELEASE_LABEL=v37 \
    --image registry.fly.io/$FLY_APP_NAME:$REPO_NAME.v37 \
    --update-only
```

## Recovery

Some of our apps are on the larger end. Mechanic uses upwards of 500 Machines, for example. Lots of things can go wrong. Here's some documentation on that:

[Recovering from deploy failures](/technical/fly/deploys/recovering-from-deploy-failures)

## Strategies

We use "immediate" in environments where deploys are manually initiated, and "bluegreen" wherever deploys are automatically initiated.

{% hint style="warning" %}
Immediate deploys finish quickly, but the actual Machine updates happen asynchronously, and may take longer. Usually they're fast, but I've seen them take more than 15min on occasion.
{% endhint %}

{% hint style="info" %}
"Why not use a strategy (like `bluegreen`) that guarantees the health of new Machines before putting them into service?"

* This takes *so much time*. So much time. Deploys are not fast, and they're hard to interrupt, and when interrupted `flyctl` tries to roll back the change, and when hundreds of Machines are in play this process is kinda brittle.
* This doubles the size of our Machine pool, which doubles the number of Postgres and Redis connections in play. This hasn't actually been a problem, but it's .. you know, it's something to think about.
  {% endhint %}

### Configuration

Our GitHub org has an org-level variable in place: `FLY_DEPLOY_STRATEGY=bluegreen`. This makes it the default value for all repos and their environments.

Each repository's *production* environment has an env-level variable in place: `FLY_DEPLOY_STRATEGY=immediate`. This makes it the effective value for that environment, and that environment alone.

## Release commands

Fly supports "release commands", which are automatically invoked during deploy, right before updating Machines with new images.

In apps that run Sidekiq, we use this feature it to issue "quiet" commands to all of our Sidekiq processes.

{% code title="fly.toml excerpt" %}

```toml
[deploy]
# deployment is done (and configured) via shared workflow. see:
# https://github.com/lightward/.github-private/blob/main/.github/workflows/fly-deploy.yml
# except for this part, where we have an app-specific interest in quieting sidekiq before release
release_command = "bin/rake sidekiq:quiet"
```

{% endcode %}

Once this happens, no jobs will be performed. Jobs will be automatically resumed as Machines come back online after the deploy.


# Recovering from deploy failures

{% hint style="info" %}
In this section, "retry" means "use GitHub Action's retry button on the failed run".
{% endhint %}

## Build failures

You *might* need to destroy the Fly builder app. It'll get auto-created again when you retry, which is what you should do after destroying the builder app.

## Docker failures

Just retry. It's fine. :)

## Release command failures

Just retry. It's fine. :)

## Machine update failures

Start by surveying the scene, to see how many machines are on the new image vs the old one, or in `replacing` vs `failed` vs `created` status.

```
$ fly m list -a $FLY_APP_NAME
```

### Total machine update failure, i.e. the release command succeeded but no Machines were updated at all

If you're here, the app is probably online but no longer processing background jobs (because all the Sidekiq processes were instructed to enter quiet mode during [the release command](/technical/fly/deploys#release-commands)).

Handle this by rebooting one of the worker\_autoscale machines. That should be enough to start bringing machines back online.

```
$ fly m list -a $FLY_APP_NAME | grep worker_autoscale
$ fly m restart MACHINE_ID
```

Once you've verified that the app is doing work again, wait for it to catch up on the run backlog, and then retry the deploy.

### **A minority of machines were successfully updated**

Manually redo the deploy.

Do this using a [CLI deploy](/technical/fly/deploys#cli-deploys), using the Docker image URI from the build step.

### A majority of machines were successfully updated

Manually update the rest of the machines.

Start by examining `fly m list -a $FLY_APP_NAME`, and build a list of machine IDs that are stuck on the old image.

For each one, do something like this:

```sh
fly m update 328756e9f52758 \
  --env RELEASE_LABEL=v62-3-ga38cb23a \
  --image registry.fly.io/$FLY_APP_NAME:locksmith-api.v62-3-ga38cb23a
```

### Sometimes a machine will get stuck and you'll need to outright destroy it

`fly m destroy MACHINE_ID`

Add `--force` if the machine is stubborn and won’t stop.

and then use `fly scale count` to scale back up to the desired machine count. Search fly scale count in the internal slack and you'll see example usage.


# Rough edges

Fly is fantastic. Super happy to be on it.

These are the rough edges we've bumped up against, and (when applicable) how we handle it.

## Fly Proxy

* [auto-stop](https://fly.io/docs/apps/autostart-stop) doesn't seeeeeem to work properly when websockets are in the mix

## flyctl

### apps

* restart
  * doesn't support `--process-group`
    * workaround (including backgrounding each Machine's individual restart command):
      * `fly m list -a $APP | grep $PROCESS_GROUP | awk NF | awk '{ print "fly m restart " $1 " &;" }'`
  * slow for restarting large numbers of Machines, and halts if any individual restart fails
    * workaround: use `fly m restart $ID &` instead
    * addressed in [Restarting apps](/technical/fly/restarting-apps)

### machines

* status
  * no machine-readable output; we regex our way through it to get Machine status
    * nb: `--display-config` exists, but that's for something else
  * doesn't include healthchecks
    * `fly checks list -a $app | grep $machine_id`

### scale

* count
  * it seems to grab a lease on *all* Machines at once, even when scoped by `--process-group`, which means `fly scale count` commands can't be run concurrently
    * no workaround


# SSH

A [rough edge](/technical/fly/rough-edges): `fly ssh console` doesn't support addressing a specific Machine.

## Connecting to a random Machine

```sh
$ fly ssh console -a $FLY_APP_NAME
$ $ bin/rails c
```

## Connecting to a specific Machine

This will display an interactive list of Machines to choose from. Good for small numbers of Machines, not great for large ones.

```sh
$ fly ssh console -a $FLY_APP_NAME -s
```

## Connecting to a specific Machine address for a given app

When an app has hundreds of Machines, it's faster on average to just look up the IP address of the desired Machine and pass that back to `fly ssh console`.

```sh
# get the Machine's IPv6 address
$ fly m status $MACHINE_ID

# use that address here
$ fly ssh console -a $FLY_APP_NAME -A $IP_ADDRESS
```


# Unusual consoles

Let's say you have an image constructed from .. who knows where.

Let's say you have a repo that uses a given Fly app to do a `fly deploy --build-only` thing, prepping an image for use elsewhere.

Let's say you want to run a console using that image in a Fly app environment which is *destined* to receive that image (i.e. destined to have its machines updated to use this image). Let's say you want to do this before that glorious destiny arrives. Maybe you want to run some helpers that this image contains, or maybe you want to run a migration that this image contains, or or or or or or.

Assuming the build happened using `--image-label $IMAGE_TAG`, this may help you on your quest:

```
fly console -a $EXALTED_APP_NAME -i registry.fly.io/$HUMBLE_APP_NAME:$IMAGE_TAG
```


# GitHub


# Dependabot

## Secrets

We use environment variables and secrets pretty heavily. Dependabot *only* gets to use these when responding to a `pull_request_target` event -- it's not a thing during `pull_request`. This is relevant, because some of our integration tests need to talk to a deployment environment.

{% hint style="warning" %}
Performing any automation on untrusted code is risky, and that's one way to describe what happens when we run tests on Dependabot pull requests. We use strictly separated environments to keep risk at an acceptable level.
{% endhint %}

## Automerge

This workflow sets up Dependabot pull requests for auto-merging via squash commit.

Note that it runs on `pull_request_target`. As with secrets, we use this event so that Dependabot qualifies for the necessary permissions.

{% code title=".github/workflows/dependabot.yml" %}

```yaml
name: Dependabot

on: pull_request_target

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot:
    name: Auto-merge
    if: github.actor == 'dependabot[bot]'
    runs-on: ubuntu-latest
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
      - name: Enable auto-merge
        run: gh pr merge --auto --squash "$PR_URL"
        env:
          PR_URL: ${{ github.event.pull_request.html_url }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

{% endcode %}

## Ruby repositories

Note that `BUNDLE_ENTERPRISE__CONTRIBSYS__COM` is defined as a Dependabot secret, at the organization level.

Note also that `registries` doesn't explicitly include rubygems.org. Don't love that, but rubygems.org appears to be included in practice anyway, so here we are.

Posting this largely so that anyone searching for Sidekiq Pro or Enterprise and Dependabot has something to find. :)

{% code title=".github/dependabot.yml" %}

```yaml
version: 2

registries:
  contribsys:
    type: rubygems-server
    url: https://enterprise.contribsys.com/
    token: ${{ secrets.BUNDLE_ENTERPRISE__CONTRIBSYS__COM }}

updates:
  - package-ecosystem: bundler
    directory: /
    registries:
      - contribsys
    schedule:
      interval: daily
    # appears to be required for this package manager to work at all
    insecure-external-code-execution: allow

  - package-ecosystem: docker
    directory: /
    schedule:
      interval: daily

  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: daily
```

{% endcode %}


# Migrations

{% hint style="info" %}
For the purposes of this page, a "migration" is a change to a Postgres database schema.
{% endhint %}

Rails scans a database's schema when ActiveRecord first connects, and it keeps its knowledge cached.

This means that schema changes (like adding or removing columns) need to be paired with an app reboot.

## Not-particularly-recommended path

This path is probably most suitable if you're doing deep, backwards-incompatible changes to the database schema.

1. Write standard Rails migrations for your pull request.
2. Merge it.
3. Release your code.
   * Important: until you reach and complete step #5, you'll have new code running with an old database schema. Plan ahead for this part, and mitigate user impact however you can.
4. Run `bin/rake db:migrate` in the deployment environment.
5. [Restart the app.](/technical/fly/restarting-apps) Wait for success.

## Recommended path

This path is probably most suitable if you're doing things that can be done idempotently *and* in a way that won't break old application code.

1. Write migrations for your pull request, using idempotent raw SQL.
2. Merge the pull request containing the migrations.
3. Before releasing the code, run the migrations manually in the target environment using `cb psql $CLUSTER_ID --role application`. Verify that your db changes are working properly, and that the db continues to be healthy.
   * :warning: That `--role application` flag is important! The app itself connects using this role, and (for continuity/consistency/predictability) it needs to have ownership over the objects it uses. So, when you're manually running migrations, it's important to use the same role as the app itself -- i.e. `application`.
4. Release your code changes, which gives you an app restart for free. Wait for success.
5. Run `bin/rake db:migrate` in the deployment environment. This is a semi-redundant step: you already manually ran the changes in step 3. Doing it this time won't fail, because you wrote idempotent migrations. Doing it *this* time also updates the private Rails bookkeeping table called `schema_migrations`, declaring at the Rails level that the database schema is up to date.
   * You can use `fly console -a $FLY_APP_NAME -s` to open up a console on an already-running machine. (You can also open a console using a different build/image! For that, see [Unusual consoles](/technical/fly/unusual-consoles).)

### Idempotence

Idempotent code is code that has side effects, but only creates those side effects *once* -- even if it's run *more* than once.

This is useful in database land! Idempotent migration code can be run more than once without errors. It's like saying "hey, make this change, but only if it wasn't already made".

Rails has some conveniences for this -- look for `if_exists:` and `if_not_exists:` in <https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html>.

However, it can be useful to stick to raw SQL, for easy pasting into psql.

{% hint style="info" %}
For functions, use `CREATE OR REPLACE FUNCTION`. Functions are stateless (unlike tables and indexes!), so it's okay to have the function created ahead of time by a human, and then recreated during the actual Rails migration execution. This still counts as idempotent behavior, because the function's existence and behavior remain consistent even when the migration SQL is re-run.
{% endhint %}

#### Example migrations

```ruby
class IndexExample < ActiveRecord::Migration[7.1]
  disable_ddl_transaction!

  def up
    execute("CREATE INDEX CONCURRENTLY IF NOT EXISTS index_input_list_items_on_input_list_id_and_id ON public.input_list_items USING btree (input_list_id, id)")
  end

  def down
    execute("DROP INDEX CONCURRENTLY IF EXISTS index_input_list_items_on_input_list_id_and_id")
  end
end
```

```ruby
class ColumnExample < ActiveRecord::Migration[7.1]
  def up
    execute("alter table shops add column if not exists shopify_country_code text null")
    execute("alter table shops drop column if exists shopify_iana_timezone_utc_offset")
  end

  def down
    execute("alter table shops drop column if exists shopify_country_code")
    execute("alter table shops add column if not exists shopify_iana_timezone_utc_offset integer null")
  end
end
```

```ruby
class FunctionExample < ActiveRecord::Migration[7.1]
  def up
    execute(<<~sql)
      CREATE OR REPLACE FUNCTION public.contains_case_insensitive_match(jsonb_array jsonb, value text) RETURNS boolean
      LANGUAGE plpgsql IMMUTABLE
      AS $$
      declare
        element TEXT;
      begin
        value := trim(lower(value));
        for element in select jsonb_array_elements_text(jsonb_array)
        loop
          if trim(lower(element)) = value then
            return TRUE;
          end if;
        end loop;
        return FALSE;
      end;
      $$;
    sql
  end

  def down
    execute(<<~sql.squish)
      DROP FUNCTION IF EXISTS public.contains_case_insensitive_match(jsonb, text);
    sql
  end
end
```


# Lightward InfoSec and Privacy Policies

**Scope:** Applies to Locksmith and Mechanic and other services processing customer personal data.

## IT Governance & Policy Oversight

\
**Applies to:** All Lightward services and operations

**Purpose:** Ensure information security practices align with Lightward’s overall strategy of delivering secure, reliable SaaS services.

**Scope:** Covers all staff and contractors working on Lightward services.

**Practices:** Policies are created and reviewed to reflect current operations. Policies are communicated during onboarding and when changes occur.

***

## Privacy & Incident Response Program

\
**Applies to:** All Lightward services and systems that handle personal data

**Purpose:** Define how Lightward collects, protects, and responds to incidents involving personal data.

**Practices:** Data minimization, encryption in transit and at rest, staff awareness training, and incident response procedures. Incidents are reported to <security@lightward.com> and customers notified within 72 hours if required.

***

## Data Protection Policy

**Purpose:** Ensure data is collected, stored, and processed securely.

**Practices:** Encrypt data in transit and at rest, restrict admin access, promptly revoke access upon termination, retain data only as long as needed, and require vendors with equivalent protections.

***

## Records Retention Policy

**Purpose:** Define retention of electronic and paper records.

**Practices:** Retain data only as long as necessary for service delivery. Customer data may be deleted upon request. Email and logs retained according to operational needs.

***

## Access Control Policy

**Purpose:** Ensure access is restricted to authorized individuals.

**Practices:** Unique accounts for all staff, least privilege access, MFA on critical systems, prompt revocation upon staff departure.

***

## Acceptable Use Policy

\
**Applies to:** All staff and contractors using Lightward systems and data

**Purpose:** Set expectations for responsible use of Lightward systems, accounts, and data.

**Scope:** Covers all internal systems (Google Workspace, GitHub, Fly.io, Shopify Partner, etc.), devices used for Lightward work, and any customer data accessed in the course of providing services.

**Practices:**

* Keep accounts and devices secure, including use of MFA.
* Report suspected incidents immediately to <security@lightward.com>.

***

## Password & Authentication Policy

**Purpose:** Define password and authentication standards.

**Practices:** Passwords must be confidential, unique per account, with complexity enforced by systems. MFA required for critical systems. Single Sign-On is supported where available (Google Workspace).

***

## Device & Endpoint Security Policy

**Purpose:** Protect staff devices that access Lightward systems.

**Practices:** Devices are kept updated.

***

## Patch & Vulnerability Management Policy

**Purpose:** Ensure timely updates to systems and software.

**Practices:** Endpoints and dependencies are patched when updates are available. Dependencies scanned regularly with tools such as GitHub Dependabot and CodeQL.

***

## Monitoring & Event Review Policy

**Purpose:** Detect and respond to abnormal events.

**Practices:** Application and system events are monitored using tools like Rollbar and New Relic. Alerts are reviewed to identify potential incidents. Logs retained to support investigation.

***

## Development Practices Overview

**Purpose:** Outline software development security practices.

**Practices:** Source code managed in GitHub, with separate development, staging, and production environments. Access restricted to authorized staff. Dependencies updated regularly. Automated scanning with CodeQL and GitHub tools.

***

## Business Resilience Policy

**Purpose:** Maintain availability of services.

**Practices:** Services hosted in AWS/Fly.io with redundancy. Backups and recovery plans ensure services can be restored. Operational risks discussed as they arise, with mitigations implemented as needed.

***

## Web Server Security Configuration Policy

**Purpose:** Define standards for hosted web applications.

**Practices:** All traffic encrypted via TLS. Hosting managed on Fly.io with hardened configurations. Patches applied as provided by the platform. Administrative access requires MFA.

***

## Public Privacy Policy&#x20;

**Purpose:** Provide transparency to customers on how Lightward handles personal data.

**Practices:** Lightward’s external privacy policy is published publicly and available to all customers.

URL: <https://lightward.inc/privacy-policy>

***


