Apps

Eigene App bauen

Die App ist deine Webseite: im Look von Samy, mit samy.js und Anmeldung per Session-Token.

Als Markdown öffnen

Eine App ist eine Webseite, die du selbst baust. Samy zeigt sie als Kachel im Tab Apps und öffnet sie unter dem Kopf der App, so breit wie der Bildschirm. Du hast alle Freiheit von HTML, und mit samy.css sieht deine Seite trotzdem aus wie Samy.

Kanal oder App

KanalApp
So geht esDein Tool schickt Nachrichten, Samy zeigt sie untereinander.Die ganze App ist deine Webseite.
Du brauchstnur HTTP-Aufrufeeinen Server, der die Seite über https:// ausliefert
Gut fürMeldungen: ein Server fällt aus, ein Deploy schlägt fehl, eine Bestellung kommt reinArbeit mit Listen und Daten: Rechnungen freigeben, Bestellungen ansehen, Tickets beantworten
Aussehenimmer wie Samymit samy.css wie Samy, sonst frei
Bedienung kommt an alsWebhook-Ereignisnormale Anfrage deiner Seite an deinen Server

Fang mit einem Kanal an, wenn dein Tool sich vor allem melden soll. Auch dort gehen Knöpfe, Schalter, Diagramme und Tabellen, als Elemente in Nachrichten. Eine App lohnt sich, wenn du immer wieder etwas nachsehen oder abarbeiten willst.

Ideen, was eine App sein kann:

  • Rechnungen freigeben: Jede offene Rechnung als Karte. Wisch nach rechts gibt sie frei, nach links lehnt sie ab.
  • Server und Deploys: Status deiner Dienste, letzte Deploys, Knöpfe für Neustart und Rollback.
  • Freigaben: Pull Requests oder Spesen ansehen und mit einem Tipp freigeben.
  • Support-Postfach: Neue Anfragen lesen und kurz antworten.
  • Bestellungen: Die Bestellungen deines kleinen Shops von heute, offen und versendet.

Wer Shopify kennt: Es ist dasselbe Prinzip wie bei eingebetteten Shopify-Apps mit App Bridge und Session-Token.

Du brauchst: einen Server mit https-Adresse, der deine Seite ausliefert.

1. Die Seite bauen

Eine normale HTML-Seite. Zwei Dateien von Samy machen sie zur App: samy.css für das Aussehen und samy.js für die Verbindung zur App.

html
<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="https://packlets.app/embed/samy.css">
    <script src="https://packlets.app/embed/samy.js"></script>
  </head>
  <body>
    <main class="samy">
      <div class="samy-row samy-spread">
        <div>
          <p class="samy-label" id="firma">Rechnung</p>
          <p class="samy-big" id="betrag">–</p>
          <p class="samy-sub" id="info"></p>
        </div>
        <span class="samy-chip" id="offen"></span>
      </div>
      <div class="samy-row">
        <button class="samy-button is-secondary samy-grow" onclick="entscheide('ablehnen')">Ablehnen</button>
        <button class="samy-button samy-grow" onclick="entscheide('freigeben')">Freigeben</button>
      </div>
    </main>
    <script>
      let rechnung;
      async function naechste() {
        const res = await samy.fetch("/api/rechnungen/naechste");
        rechnung = await res.json();
        document.getElementById("firma").textContent = rechnung.firma;
        document.getElementById("betrag").textContent = rechnung.betrag;
        document.getElementById("info").textContent = `Fällig am ${rechnung.faellig} · ${rechnung.nummer}`;
        document.getElementById("offen").textContent = `${rechnung.offen} offen`;
      }
      async function entscheide(aktion) {
        const res = await samy.fetch(`/api/rechnungen/${rechnung.id}/${aktion}`, { method: "POST" });
        if (!res.ok) return samy.toast("Das hat nicht geklappt");
        samy.haptic("success");
        naechste();
      }
      naechste();
    </script>
  </body>
</html>

Die Klassen wie samy-row und samy-button kommen aus samy.css. Alle stehen in der Referenz.

2. Die Seite zur App machen

Leg in Samy ein Packlet an und wähle Eigene App bauen. Dann schickt dein Tool einmal die Adresse deiner Seite:

Shell
curl -X PUT https://api.packlets.app/v1/ui \
  -H "Authorization: Bearer $SAMY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"page": "https://dein-server.de/rechnungen"}'

Antwort: {"ok": true, "page": "https://dein-server.de/rechnungen"}. Samy zeigt Verbunden!, und die Kachel steht im Tab Apps.

  • Nur https://. Im Body steht nur page.
  • Beim ersten Öffnen zeigt Samy die Adresse deiner Seite und fragt nach, bevor es sie lädt. Wechselst du später auf eine andere Adresse (anderer Host oder Port), fragt Samy erneut.
  • Eine andere Adresse setzt du mit einem neuen PUT.
  • Einmal reicht. Was die Seite zeigt, bestimmt danach dein Server.

3. Mit der App sprechen: samy.js

Über das Objekt samy nutzt deine Seite, was nur die App kann:

AufrufWas passiert
samy.toast("Gespeichert")Kurze Meldung in der App.
samy.haptic("success")Vibration.
samy.open("https://…")Öffnet eine Adresse im Browser.
await samy.fetch(url, init)Wie fetch(), aber mit Anmeldung. Siehe nächster Schritt.
samy.ready()Seite ist fertig, Samy blendet den Platzhalter aus (mit data-ready="manual").
samy.storage.set("ort", "Berlin")Speichert Text auf dem Gerät, getrennt nach Konto.

Samy hält geöffnete Seiten im Speicher. Kommt jemand zurück, ist deine Seite sofort da, ohne neu zu laden, und bekommt das Ereignis samy:activated. Samys Farben stehen als CSS-Variablen bereit (--samy-bg, --samy-text, --samy-cta …).

Im normalen Browser gibt es die App nicht, und die Aufrufe schlagen fehl. Frag deshalb vorher samy.inSamy ab, wenn deine Seite auch dort laufen soll:

html
<script>
  if (!samy.inSamy) document.body.textContent = "Öffne diese Seite in Samy.";
</script>

Alle Aufrufe stehen in der Referenz.

4. Wissen, wer die Seite öffnet

Deine Seite ist öffentlich erreichbar. Zeigt sie private Daten, muss dein Server wissen, dass die Anfrage wirklich aus deiner App in Samy kommt. Ein eigenes Login brauchst du dafür nicht: Samy weiß schon, wer angemeldet ist, und bestätigt es deinem Server mit einem Session-Token.

In SamyDeine Seiteruft samy.fetch() auf, das holt ein frisches Token
samy.token()Token
DazwischenSamystellt ein JWT aus, 60 Sekunden gültig, signiert mit deinem Schlüssel
Bearer-Tokendeine Daten
Bei dirDein Serverprüft das Token und weiß, wer die Seite öffnet
  1. Deine Seite ruft samy.fetch() auf. Das holt bei Samy ein frisches Token und schickt es als Authorization: Bearer … an deinen Server.
  2. Dein Server prüft das Token. Stimmt es, weiß er, wer die Seite öffnet.

Das Token ist ein JWT, 60 Sekunden gültig und signiert mit einem Geheimnis, das nur du und Samy kennen: dem SHA-256-Hash deines Schlüssels. Geprüft wird auf deinem Server:

import hashlib, os
import jwt  # pip install pyjwt

SECRET = hashlib.sha256(os.environ["SAMY_KEY"].encode()).hexdigest()
PACKLET_ID = os.environ["SAMY_PACKLET_ID"]

def samy_user(request):
    token = request.headers.get("Authorization", "").removeprefix("Bearer ")
    claims = jwt.decode(token, SECRET, algorithms=["HS256"], audience=PACKLET_ID)
    return {"id": claims["sub"], "email": claims.get("email"), "name": claims.get("name")}

Schlägt die Prüfung fehl, werfen beide Funktionen einen Fehler. Antworte dann mit 401.

  • SAMY_PACKLET_ID ist die ID deines Packlets. Du bekommst sie in der Seite mit (await samy.info()).packlet_id, und sie steht in jedem Webhook-Ereignis als packlet_id.
  • Kein Cookie, keine Sitzung. samy.fetch() holt für jede Anfrage ein frisches Token.
  • Ohne JWT-Bibliothek kann dein Server das Token auch von Samy prüfen lassen.

Achtung: Der Schlüssel gehört auf deinen Server, nie in die Seite. Was im HTML oder JavaScript steht, kann jeder lesen.

5. iframes und fremde Inhalte

Deine Seite selbst ist kein iframe: Samy zeigt sie in einem eigenen Browser in der App. Zwei Dinge solltest du trotzdem regeln.

Fremde Inhalte in deiner Seite. Bettest du ein iframe von einer anderen Adresse ein (Video, Karte, Widget), kann es samy nicht benutzen. Samy gibt der Seite beim Laden einen geheimen Brücken-Schlüssel, den nur deine Seite sieht, und ignoriert jeden Aufruf ohne ihn. Was das iframe sonst tut, entscheidest du:

html
<iframe src="https://www.youtube-nocookie.com/embed/…"
        sandbox="allow-scripts allow-same-origin"
        referrerpolicy="no-referrer"></iframe>
  • sandbox immer setzen und nur freigeben, was das iframe braucht. Ohne allow-top-navigation kann es deine Seite nicht wegschicken, ohne allow-popups keine Fenster öffnen.
  • Fremde Skripte (Werbung, Tracking) direkt in deiner Seite laufen mit allen Rechten deiner Seite, auch mit samy.fetch(). Bind nur Skripte ein, denen du vertraust.
  • Inhalte von Nutzern nie als HTML einsetzen (innerHTML), sondern als Text (textContent).

Deine Seite in fremden Seiten. Deine Adresse ist öffentlich. Eine andere Webseite könnte sie in ein iframe laden und Nutzer zu Klicks verleiten. Ein Token bekommt sie dort nicht, weil es Samy dort nicht gibt. Verbiete das Einbetten trotzdem mit einem Header deines Servers:

HTTP
Content-Security-Policy: frame-ancestors 'none'
X-Frame-Options: DENY

Samy stört das nicht, denn die App bettet deine Seite nicht in ein iframe ein.

Damit es sich wie eine App anfühlt

  • Weißer Hintergrund, kein eigener Rahmen und kein Schatten außen herum.
  • Kein eigener Kopf. Name und Avatar zeigt Samy schon. Was wichtig ist, gehört nach oben.
  • Tippflächen mindestens 44 Pixel hoch, Text mindestens 12 Pixel.
  • Kein Hover, keine Pop-ups, kein alert(). Für Meldungen gibt es samy.toast().
  • Links auf deiner eigenen Adresse bleiben in der App. Links zu anderen Adressen öffnen sich im Browser.
  • Testen kannst du im Browser in Handy-Breite, zum Beispiel mit 375 Pixeln in den Entwicklerwerkzeugen.

Benachrichtigen

Eine App kann auch Nachrichten schicken, mit demselben POST /v1/push wie ein Kanal. Die Nachricht kommt als Push, und die Kachel zeigt einen Zähler. So meldet sich zum Beispiel die Rechnungs-App, wenn neue Rechnungen warten. In der App selbst siehst du nur deine Seite, keinen Verlauf.

Ausprobieren

Die beiden Seiten oben sind echte Apps:

  • Wetter zeigt, wie frei du gestalten kannst. Jedes neue Konto hat sie in Samy unter Apps.
  • Konto zeigt das Session-Token: Die Seite holt bei jeder Anfrage ein frisches Token, ihr Server prüft es und zeigt deinen Namen. Im Browser zeigt dieselbe Seite nur einen Hinweis, denn dort gibt es kein Token.

Checkliste

  • Adresse der Seite mit PUT /v1/ui und {"page": "https://…"} geschickt.
  • <meta name="viewport" content="width=device-width, initial-scale=1"> gesetzt.
  • samy.css und samy.js eingebunden.
  • Anfragen an deinen Server laufen über samy.fetch().
  • Dein Server prüft bei jeder Anfrage Signatur, Ablauf und aud des Tokens.
  • Der Schlüssel steht nur auf dem Server.
  • Fremde iframes mit sandbox, deine Seite mit frame-ancestors 'none' ausgeliefert.

Wie es weitergeht

  • Alle Aufrufe von samy.js, das Token und samy.css: Referenz.
  • Feld, Antwort und Fehler von PUT /v1/ui: PUT /v1/ui.