Sign in
Feedback docs

Signed in users

Sign a short lived token on your server so answers carry a verified user and traits, with Node and Python examples.

When people are signed in to your product, your server can vouch for them. It signs a short lived token with a secret only you and OneStar know, your page hands the token to the form, and every answer then carries a verified user and the traits you chose, such as their plan. Signed in users come with Pro and Max.

How it works#

  1. You make a signing secret in OneStar and store it on your server.
  2. When a signed in person loads a page with a form, your server signs a token for them.
  3. Your page passes the token to the form. The form sends it with the answer.
  4. OneStar checks the signature and the expiry, and stores the answer as a signed in user's, with their traits.

A token is a JSON Web Token signed with HS256. Sign it on your server only. Never put the signing secret in a web page, in browser code or in an app binary: anyone who has it can answer as any of your users. A native app asks your server for a fresh token each time it shows a form.

Make a signing secret#

Secrets belong to your workspace, so one secret works for every form. To make one, open a form, go to Settings, and under Who may answer choose Signed in users of your product. The Signing secrets list appears below it.

  • Make a secret shows the new secret once. Copy it into your server's configuration right away.
  • The secret starts with oss_. Use the whole string, prefix included, as the HMAC key.
  • Two secrets can be live at once, so you can rotate without breaking sign in: make a second secret, switch your server to it, then revoke the older one.
  • Revoke stops a secret at once. Tokens signed with it are no longer accepted.

You do not have to limit a form to signed in users to use tokens. On a form open to anyone, a valid token simply marks the answer as a signed in user's.

Claims#

ClaimRequiredWhat OneStar does with it
subYesYour user's id, a string of 1 to 200 characters. OneStar stores only a keyed hash of it, so answers from the same user on any device are one person.
expYesExpiry, in Unix seconds. At most 24 hours after the time OneStar checks it. An hour or less is a good choice.
iatNoIssued at, in Unix seconds. Optional.
emailNoKept with the answer as its contact, sealed, only when the form is set to keep emails, so you can reply by email without the person typing an address. Otherwise ignored.
plan, platform, app_version, country, cohortNoTraits. Each value is a string, number or boolean, cut to 40 characters.
traitsNoAn object with up to five custom traits. Only its first five entries are read. Keys use lower case letters, digits and underscores, start with a letter and are at most 24 characters. Values follow the same rules as the named traits.

The token's header must say "alg": "HS256"; any other algorithm is refused. Other claims, such as aud or iss, are ignored and never stored. A token longer than 4,096 characters is ignored.

A trait describes the person, not one answer: each new token replaces the traits stored for that user. Keep traits to short labels like pro or 2026-10, never anything personal.

Sign a token on your server#

Both samples sign the same claims. Any HS256 JWT library in another language works the same way.

Node#

Uses the jose package (npm install jose).

sign-token.mjs
import { SignJWT } from "jose";

// The whole secret from OneStar, oss_ prefix included, kept in your server's environment.
const secret = new TextEncoder().encode(process.env.ONESTAR_SIGNING_SECRET);

export async function onestarToken(user) {
  return new SignJWT({
    email: user.email,
    plan: user.plan,
    traits: { team_size: user.teamSize },
  })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject(String(user.id))
    .setIssuedAt()
    .setExpirationTime("1h")
    .sign(secret);
}

Python#

Uses PyJWT (pip install pyjwt).

sign_token.py
import os
import time

import jwt

# The whole secret from OneStar, oss_ prefix included, kept in your server's environment.
SECRET = os.environ["ONESTAR_SIGNING_SECRET"]


def onestar_token(user: dict) -> str:
    now = int(time.time())
    claims = {
        "sub": str(user["id"]),
        "iat": now,
        "exp": now + 3600,
        "email": user["email"],
        "plan": user["plan"],
        "traits": {"team_size": user["team_size"]},
    }
    return jwt.encode(claims, SECRET, algorithm="HS256")

Serve the token to the signed in person's page, for example in the HTML your server renders or from a route that only answers signed in sessions.

Pass the token to the form#

On a web page, call onestar.identify with the token. It applies to every form on the page, and forms already drawn pick it up. Calls made before the loader runs are queued.

index.html
<script>
  window.onestar = window.onestar || { q: [] };
  onestar.q.push(["identify", "<token from your server>"]);
</script>
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
<onestar-feedback form="f_hemsz66yupbd76d2"></onestar-feedback>

Once the loader has run, onestar.identify(token) works directly, and onestar.identify(null) signs the person out of the form. You can also put the token on one element with the user-token attribute:

index.html
<onestar-feedback form="f_hemsz66yupbd76d2" user-token="<token from your server>"></onestar-feedback>

In a native app's web view, add the token to the form page's address after #token=. The part after # never reaches a server, and the page removes the token from its address once it has read it. See Native apps.

Web view address
https://onestar.sh/f/<your form's page>/embed?platform=ios&app_version=4.2#token=<token from your server>

Forms only for signed in users#

Under Who may answer, Signed in users of your product takes answers only with a valid token. Without one, the form shows your sign in message instead of the questions (set it under the same setting), and the element fires onestar:error with the code identity_required once. The form does not ask for a contact, since the token says who answered. Under Contact, choose whether the token's email claim is kept as their contact.

What you see#

  • Results shows signed in users as their own group in who answered, apart from anonymous answers and answers that left a typed contact. Filter by it with Signed in user, or level=signed_in in the REST API.
  • Traits become filters and splits in Results: pick Trait to split answers by one, or filter to one value. In the API, pass trait and trait_value.
  • The CSV export has a column for each trait, such as trait:plan.
  • A form set to one answer per person counts a signed in user once per version, on every device.
  • Replies you write reach the person wherever your page passes their token, not only on the device they answered from.

Never shown, to you or anyone: the sub itself (OneStar keeps only a keyed hash), and claims outside the list above. The email from a token is shown only to your workspace, when you open the contact on an answer or export with contacts. Traits your page states with onestar.set are kept apart from signed traits and are never checked; see Context and placements.

When a token is not accepted#

OneStar does not say why a token was refused. On a form open to anyone, an invalid token is treated as no token: the answer is stored as anonymous. On a form only for signed in users, the person sees your sign in message and the element fires onestar:error with identity_required. Check these in order:

  1. The token is signed with HS256, and the key is the whole secret including oss_, as text, not base64 decoded.
  2. The secret is still live under Signing secrets, not revoked.
  3. sub is a string of 1 to 200 characters. A number fails: send "42", not 42.
  4. exp is in Unix seconds, in the future, and no more than 24 hours ahead. Check your server's clock.
  5. The token is under 4,096 characters, with no spaces or quotes around it.
  6. Your workspace is on Pro or Max. On other plans tokens are ignored.
  7. The token reached the form: set user-token or call onestar.identify before the person answers. If your page signs the person in after the form is drawn, call onestar.identify again.
  8. The secret was made in the same OneStar workspace that owns the form.

To check a token by hand, decode it with any JWT tool and compare its header and claims with the table above.