An install flow is a trust flow that happens to use OAuth
The person installing your app is deciding whether to give you access to something they care about, on a screen you did not design, in about ninety seconds.
Vishal Chiniwar Co-founder and CTO 13 July 2026
Where installs actually fail
The instinct is to instrument the install flow from the moment somebody lands in your app. By then most of the losses have already happened.
The sequence is roughly: they see your listing, they press install, they are sent to a consent screen owned by the platform, they read a list of permissions written by the platform in the platform's words, and they either continue or they do not. Everything up to that decision happens on surfaces you do not control, and the largest single drop is on that screen.
Which means the design work that matters most is not in your onboarding. It is in what the permission list says, how many items are on it, and what the person was told to expect before they got there.
The listing is the first state, and it is not yours either
Before any of the states in that table, there is a page in a marketplace that you wrote inside somebody else's template, and it is doing more work than most teams give it credit for.
Two things on it matter disproportionately. The first is whether a reader can tell, in one sentence, what the app does to their site rather than what it is. Does this thing edit my content, read it, or add something new. People are about to grant access and they are trying to predict consequences.
The second is whether the permissions are foreshadowed. If the listing says nothing about access and the consent screen then asks for write permission on everything, the gap between expectation and request is where people stop. If the listing has already said this needs to write to your collections because that is what it does, the consent screen confirms rather than surprises.
You cannot instrument any of this, because the marketplace does not give you the numbers, and that is exactly why it gets neglected. The only signal available is the ratio of installs started to listing views, if the platform reports it, and the questions that arrive in support before anybody has installed anything.
The practical version: write the listing after the permission list is final, not before, and read them together as one document. They will be read that way.
Permissions are the product's first argument
A permission list is read as a statement about what you intend to do. It is usually the first concrete information anyone has about your app, and it arrives without any of your framing attached.
Two rules have served us. Ask for the narrowest set that makes the first useful thing work, and defer everything else to the moment it is needed. An app that requests write access to everything on install, because it might eventually need it, is asking a stranger for a key to the whole building on the basis that they may one day want to visit the roof.
The second rule is to explain each permission before the consent screen rather than after, in your own words, on your own page. By the time somebody is reading the platform's phrasing it is too late to add context. A short line per permission saying what it is for converts a list of capabilities into a set of intentions.
Incremental permission requests cost you a second flow and are almost always worth it. The install completes on a small ask, the user sees something work, and the larger request arrives when they already have a reason to say yes.
Model it as a state machine, then draw the ugly states
An install has more states than the happy path suggests, and the ones that get skipped in design are the ones that generate support load.
The discipline is to enumerate every state including the ones you hope never happen, and give each one a screen with a next action. A state with no screen becomes a blank page or a spinner, and a spinner is what a person sees for the ten seconds before they decide your app is broken.
The table below is the set we design against. The last column is the one that gets skipped, and it is the one that determines whether a stuck user becomes a support ticket or a churned install.
| State | Cause | What the screen must say | Next action available |
|---|---|---|---|
| Started | User pressed install | Nothing, this state should be invisible | Automatic |
| Consent shown | Redirect to platform | Owned by the platform, not you | Approve or cancel |
| Declined | User said no | Why the permissions were needed, plainly | Try again, or read more first |
| Returned, valid | Callback with a good code | Confirmation plus the first useful thing | Continue into the app |
| Returned, invalid | Bad, reused or expired code | Something expired, this is normal | Restart the install |
| Already installed | Install run twice | You already have this, here is where | Open the app |
| Wrong account | Installed to a different workspace | Which account this landed in, explicitly | Switch account and retry |
| Insufficient rights | User lacks permission to install | What role is needed, and what to ask for | Send a request to an admin |
| Platform error | Upstream failure | This is on our side or theirs, not yours | Retry, and a support path |
| Timed out | Callback never arrived | What state the install is in, honestly | Retry safely, without duplicating |
The two states everyone skips
Wrong account and insufficient rights are the two I would check first in any install flow, including our own, because they are common and almost never designed.
Wrong account happens constantly in tools where people belong to several workspaces. The install succeeds, technically, and lands somewhere the user did not intend. From their side nothing happened, because they are looking at the workspace they meant to use. Then they install again, and now you have two installs and a confused user. The screen has to state which account it landed in, by name, and offer to switch.
Insufficient rights is the case where the person who wants your app is not the person who can approve it. If your flow ends at an error, you have lost an install that was genuinely wanted. If it ends at a screen that says what role is needed and offers to send a request to somebody who has it, you have converted a dead end into a slower yes.
Neither of these is technically difficult. Both require having decided the state exists.
Query parameters on the return leg are hostile input
The callback is the point where an install flow becomes a security surface, and it is worth being blunt about that because it looks like plumbing.
Every parameter arriving on that request came over a redirect and can be set by anybody who can get a user to click a link. The authorisation code, the state parameter, and above all any parameter describing where to send the user next.
The state parameter has to be generated by you, stored server side against the session, single use, and compared on return. Reflecting back a value you received is not verification. If it does not match, the correct behaviour is to fail the install rather than to continue with a warning, because a mismatch is either an attack or a bug and neither should complete.
Codes are single use and short lived by design. A reused code should produce the returned invalid screen, not an error page, because the most common cause is somebody refreshing the callback URL, which is an ordinary thing for a person to do.
Safe redirects, specifically
The redirect target is the parameter most likely to be handled carelessly, because the feature it enables is convenient: send the user back to the page they started from.
An allow list of paths is the only approach I would defend. Not a domain check, which is easy to get subtly wrong against subdomains and userinfo tricks. Not a scheme and host comparison assembled by hand. A list of permitted destinations, matched exactly, with a default when there is no match.
The convenience version, taking whatever the parameter says and validating that it starts with your domain, has been the source of a long history of open redirects, and the reason is that URL parsing has more edge cases than anybody expects.
If you need to preserve context across the flow, put it in the server side session keyed by the state parameter rather than in the URL. Then the return leg carries no destination at all, and the whole category of problem stops existing.
Retries that do not duplicate
Any flow with a network call in the middle will be retried, by users pressing back, by browsers reissuing requests, and by your own error handling.
So the install operation has to be idempotent against a key you control. Generate an install attempt id at the start, carry it through, and make the completion step a write that either creates the install or recognises the one that exists. Then a double submit produces one install and a second confirmation screen rather than two installs and a billing question.
The timed out state deserves its own treatment. If the callback never arrives, the honest screen says the install may or may not have completed, here is how to check, and here is a retry that is safe. What it must not do is guess. A screen that says installation failed when the install actually succeeded produces a duplicate, and a screen that says success when it did not produces a support ticket with a user who believes you.
This is one of the places where our own early work was wrong. The first version of one of our Webflow apps assumed the callback would arrive, and the state where it did not was a blank page. The fix was not complicated once the state was named, which is the general pattern with all of this.
The support path belongs in the flow
Most install flows treat support as something that happens elsewhere, on a help page, after the person gives up. That is one click too far at exactly the moment somebody is deciding whether your app is worth the trouble.
Every terminal error state should carry a way to get help that does not require the user to explain what happened. That means a link that carries the state: the install attempt id, the error code, the account it was attempting, the timestamp. A support conversation that starts with all of that is a different conversation from one that starts with the app did not work.
It also has a second effect that is easy to miss. If your error screens carry a code, your support volume becomes data. You can count which failures actually reach people, which is different from what your logs suggest, because logs record everything and support records what people cared enough to report.
What to instrument
Given that most losses happen before your code runs, the instrumentation that matters is unusual.
Count installs started against installs completed, per entry point. The gap between those two is where the permission list is doing its work, and it is the number most worth moving.
Count each terminal state separately rather than as failures. Declined, wrong account and insufficient rights have completely different remedies, and aggregating them into one failure count hides all three.
Count retries per attempt id. A flow with a high retry rate and a normal completion rate is one where people are getting through by persistence, which is a design problem that success metrics conceal.
And record time to first useful action after install, because an install that completes and does nothing is a churn in a few weeks rather than a win today.
The pre launch pass
Eight items, and the first four are the ones that change conversion while the last four are the ones that stop a bad week.
Run it against your own flow rather than reading it. Most of these fail quietly and none of them announces itself in a happy path test.
Before an install flow goes live
- Every permission requested is needed for the first useful action, and everything else is deferred
- Each permission is explained in your own words, on your page, before the consent screen
- Every state in the table has a screen, including declined, wrong account and insufficient rights
- The state parameter is server generated, single use, and a mismatch fails the install
- Redirect targets are matched against an allow list of paths, not validated by string prefix
- The completion step is idempotent against an install attempt id
- Every terminal error carries a support link with the attempt id and error code
- Terminal states are counted separately, not aggregated as failures
Where teams get this wrong
Designing the happy path and treating everything else as errors. The unhappy states are where the users are, and an error page is not a design.
Requesting the permission set the roadmap needs. The install is a negotiation about the next ninety seconds, not about the product's eventual scope.
Trusting the return leg because it came from the platform. It came from a browser, over a redirect, and it can be constructed by anybody.
Putting the destination in the URL. It is convenient, it is the single most common source of open redirects, and the server side session removes the need for it entirely.
Measuring completed installs only. The interesting number is where people stopped, and that requires counting the states you would rather not have.
What good looks like
A good install flow is short, asks for less than you expected, and tells you what it is doing at every point where it could plausibly be stuck.
When something goes wrong, the screen names the specific thing, in language a person can act on, with a next step that is not go and read the documentation. When it goes right, the first thing you see is not a dashboard but the thing you installed the app to do.
The internal signal that it is working is that support tickets about installs stop being narrative and start being specific. Instead of it did not work, you get an error code and an attempt id, which means the flow told the user something true about what happened.
Where this comes from
We have built four Webflow Designer apps, three of which are public and installable today. Most of what is above is the accumulated result of getting parts of it wrong on the earlier ones and fixing them on the later ones.
The states table in particular is not a design exercise. It is the list of things that actually happened to real people, assembled backwards from support conversations, which is why it includes cases like wrong account that would not occur to anybody drawing a flow on a whiteboard.
I am not going to attach install numbers to any of this, because our volumes are not the interesting part and citing them would imply a generality we cannot support. The states are the transferable thing, and they apply whether an app is installed ten times or ten thousand.
Related reading
The workflow is fine until somebody else is editing at the same time
A Webflow app runs inside a Designer session it does not own, against a CMS somebody else may be changing, through an API with limits. All three are normal and all three break things.Vishal Chiniwar15 July 2026A prompt is not a guardrail
Instructions in a prompt are a preference. A guardrail is something that can refuse, that leaves a record when it refuses, and that you can write a test against.Vishal Chiniwar19 May 2026We built four apps before we noticed they were all the same app
Every one of them fixed something at the scale of a whole site rather than a page. It took us longer than it should have to see what that meant.Sachin Aathreyaa K M24 July 2026
Questions this raises
No. The install is a decision about the next ninety seconds by somebody who does not know you yet. Ask for what the first useful action needs, and request the rest when there is a reason attached. The second flow costs you engineering time and buys you installs.
It is not. URL parsing has more edge cases than a prefix check accounts for, and open redirects have a long history of arriving through exactly that assumption. An allow list of exact paths is the version worth defending, and putting the destination in the server side session removes the need for the parameter at all.
Insufficient rights. It converts a dead end into a slower yes, because the person wanted your app and simply could not approve it themselves. A screen naming the role needed and offering to send a request recovers installs that are otherwise silently lost.
Force them. Install to the wrong workspace deliberately. Sign in as a user without permission to approve. Refresh the callback URL. Kill the connection mid flow. Each takes a couple of minutes and each one is a state real users will reach without trying.
It costs you engineering time and buys you installs, in our experience. The install is a decision about the next ninety seconds by somebody who does not know you. A smaller ask completes, they see something work, and the larger request arrives when there is a reason attached to it.
URL parsing has more edge cases than a prefix check accounts for, and open redirects have a long history of arriving through exactly that assumption. An allow list of exact paths is defensible. Better still, keep the destination in the server side session so the return leg carries no destination at all.
Installs started against installs completed, per entry point, and each terminal state separately rather than aggregated as failures. Declined, wrong account and insufficient rights have completely different remedies, and one failure count hides all three.
Most installs fail in the first ninety seconds.
Usually on permissions or an unexplained redirect. If you are building on the Webflow platform we will compare install flows with you.
Three public Webflow Designer apps. The install flow is the part we rewrote most.