Haru Space Lab
We Built Boundaries Before Buttons — Platform Authority, Group Membership, and Human Approval
How Haru Space separated platform administration, group membership, human approval, and constrained AI drafts before adding more features.

Summary
At a glance
- We separated platform management from group membership so administrative authority did not imply chat participation or the right to act as a member.
- We preserved group proposals and votes as append-only history, while AI could only suggest a constrained space draft for a person to preview and apply.
- Failures in an administrator proposal rule and schema version taught us to align permission meaning and database changes across code, UI, and operations.
This post looks back on July 15–21, 2026, using Git history and the design and operations documents from that time. It records the week Haru Space rebuilt its permission boundaries before adding more features.
When building a service, permissions tend to appear first as buttons.
Who can create something? Who can edit it? Who should see the approve and reject buttons? Looking only at the screen, it seems enough to show or hide buttons according to a role name. That was what I thought at first, too.
But as the number of groups grew, one person began moving among several groups, and pets started taking on AI roles, the question changed.
Is the person who may see that button really the same person who may become the subject of that data?
The third week of July brought many features to Haru Space. Group governance, participation in multiple groups, onboarding for a first space, a three-language interface, and a shared feedback pattern arrived one after another. At the same time, it was a week of rebuilding boundaries that would outlast those features. We separated platform administration from eligibility to participate in a group, human identity from AI-pet identity, and creating a draft from applying it in practice.
The process was not smooth. A super administrator could not create a proposal they needed to make. We also added a new column without increasing the schema version, causing only the group approval path in production to fail. Either issue could be fixed with a small condition or a single number, but the causes ran deeper. We had not been describing the meaning of permissions and changes in the same terms across the whole system.
Does being a super administrator make someone a member of every group?
At first, the name “super administrator” carried too many meanings. Because this account inspected the platform as a whole, it could open every group and could also have a currently selected group. That made it easy for code to interpret “there is a current group” as “this person is a member of that group.”
Those are different things.
When a super administrator checks a group’s settings and status to operate the platform, that is a management view. Entering a chat room, reading messages and attachments, being counted among connected people and read states, and receiving notifications are group participation. The former falls within the platform responsibilities that require oversight. The latter is a relationship held as a member of the group.
We redrew the boundary so that the ability to inspect every group did not automatically grant a super administrator membership in all of them. An account could act as a participant in chat only in a group where an active membership had been approved or explicitly assigned. Without membership, the server blocked messages, attachments, read states, real-time connections, room invitations, and automatic entry into public rooms. The group selector also distinguished Management view from Participating instead of showing every group as though the administrator had joined it.
The reason this distinction matters lies less in the visible button than in the traces left in data. The moment an account carrying out a management inspection is counted among chat participants, added as a recipient of unread messages, or sent group notifications, platform administrative authority has quietly spread into the power to act inside the group. Hiding one button later cannot easily disentangle relationships that have already been mixed together.
We therefore changed our decision rules.
- A platform role manages platform policy and security, along with group creation and suspension.
- Group membership represents participation and a role within one particular group.
- The group currently shown on screen may only be a navigation context; it is not itself evidence of membership.
- Chat, notifications, and participant counts recheck active, approved membership on the server.
Making it easier for an administrator to see everything mattered less than making it possible for a group to explain who its actual members were.
Account approval and group participation were not the same thing either
Until then, signup approval had bundled an account and group participation together. When there was only one group, a flow that decided where a person belonged at the same time they signed up was simple. Multiple groups and requests to create new ones quickly exposed the limits of that structure.
We chose to activate new accounts other than the first-user bootstrap account without automatically creating a group membership. A login session could exist, but if it had no approved membership, it could use only group onboarding. Joining an existing group and requesting a new one became separate requests, each recording who reviewed it and when it was approved or rejected.
Again, hiding a screen was not enough. If an ordinary account without membership called a home route or API route directly, the server had to reject the request with an onboarding-required state. Group administrators could see only participation requests for their own groups, while new-group requests were reserved for the platform owner; we separated the query results themselves accordingly.
Leaving a group was also kept as a state rather than handled by deleting a row. We distinguished a voluntary departure from removal by an administrator and decided differently on rejoining—whether to restore the prior role or begin again as a regular member. Human relationships are difficult to explain with only “exists” and “deleted.”
This work gave us a clearer rule: an account identifies a person, membership describes a relationship, and a role grants authority within that relationship. We chose a structure that could explain why access had been allowed later, even though a single role value would have been more convenient at first.
We did not put human members and AI pets in the same slot
When AI pets began entering chat rooms, a different temptation appeared. Because a pet also leaves messages, putting it in the same member table as a human user seemed easier to implement. But a pet is not a person who signed up and consented, nor is it an independent member of a group.
A pet is closer to an AI tool that a group acquires or is assigned and then configures. Its display name, role, model, and visibility can differ by group, and an administrator decides which chat rooms it occupies. A user should be able to ask questions only of a pet allowed by the current group and placed in the current room.
We therefore separated human group membership from per-group pet settings and room placement. When saving an AI answer, we used a dedicated bot identity instead of disguising it as a human account. The server rechecked the questioner’s room access, the pet’s group visibility, and its placement in that room.
This boundary was more than database housekeeping. A pet answering a question does not grant that pet permission to read every document in the group. A super administrator’s ability to configure a pet does not allow its conversations to cross every group. We may call people and pets “members” in ordinary language, but they should not be stored as actors with the same rights and responsibilities.
We preserved the decision process, not just the voting result
We did not want the question of who holds group-administrator authority to remain only a value that a platform operator could overwrite at will. We built a small governance flow in which a proposal is opened for the current group, an approved member casts one approve or reject vote, and a majority determines the outcome.
The important part was not merely keeping a final approved or rejected value.
We fixed the eligible voters at the time a proposal was created and enforced one vote per person in the database as well. The proposal, approval votes, rejection votes, and final decision were stored as events appended in order. We did not create an API for editing or deleting a submitted vote. When a proposal to grant or revoke authority passed, the final status, role change, and application time were recorded in one transaction. If the target membership had become inactive immediately before the decision, the process ended with a safe rejection.
This approach is inconvenient. An operator cannot simply correct a mistaken vote, and it creates more data rows. In return, we can explain not just “who is an administrator now,” but “who became an administrator, through what decision, among which set of members.”
Group sovereignty felt less like a grand declaration and more like this kind of recoverable explanation. It was a promise that even though the platform stores a decision’s result, it will not quietly rewrite the group’s decision out of view.
First failure: the super administrator could not propose an agenda item
As soon as we made the boundaries more granular, a bug appeared in the opposite direction.
An ordinary member needed an active, approved membership in the current group to propose changing group-administrator authority. That check was correct. The problem was that we applied the same membership check again to the super administrator, who needed to register authority-granting or authority-revoking proposals at platform scope. A super administrator might open some groups in Management view without being an actual member, so the required proposal was rejected.
The solution was not to turn the super administrator back into a member of every group. We added an explicit exception only for the platform management authority to register a proposal; voting and chat participation continued to require actual membership.
This bug showed that adding more permission checks does not automatically make a system safer. Copying the same membership condition across different actions can block too much, while a single exception can spread too broadly in the other direction.
After that, we tried not to build conditions from sentences like “allow because this person is an administrator” or “reject because this person is not a member.” We first wrote down which action may be performed on which resource, and within what scope, then implemented the check. The design documentation also preserved the limit that this exception applied only to proposing an agenda item and did not create eligibility to participate in the group.
AI proposed a constrained draft instead of applying a space directly
That same week, we also built onboarding in which the AI pet Coco helped prepare a new group’s first space. Rather than implementing a long product-onboarding journey at once, we started with a small vertical flow designed to keep people from arriving at an empty space and leaving after group approval.
The user chose the space’s purpose, the first menu to show, and its atmosphere. We called AI only when the user explicitly selected Design with AI. The result was not arbitrary React code or CSS, but a constrained manifest that the existing renderer could understand. It retained only permitted blocks and internal routes, excluding external URLs, HTML, JavaScript, CSS, and executable code.
The most important stage came after generation.
We did not apply the AI result directly to the group’s space. We rendered a preview through the same renderer, and applied it only when a group administrator selected Start with this space. On save, the validated draft, currently applied content, draft revision, application version, and history were updated together in one transaction. If the expected revision no longer matched, a stale screen could not overwrite the latest settings.
We also constrained model input. The system did not automatically send raw group chat or documents; it sent only the purpose, menu, atmosphere, and short note chosen by the user. If no model was connected or a call failed, it used a deterministic local guide with the same allowed structure instead of halting the flow. Rather than resaving the entire source text for audit purposes, it retained an input hash, character count, model and prompt versions, token count, result, and processing time.
At this point, AI was not creating a project or deploying code. Its entire role was to suggest a constrained draft for a group space. Precisely because the scope was small, it let us test a principle that we would reuse later in a real flow.
AI can propose. A person reviews the rendered result and applies it. A draft is not the same thing as the applied version.
Second failure: the column existed in code, but not in production
Onboarding needed a membership state so that, after group approval, Coco’s first guide would appear only once. We added a new column in code, set a default for existing rows, and wrote the paths so that the guide would be enabled only after a new approval or rejoining.
Then a new-group approval failed in production. Rejection continued to work. The difference was that only the approval transaction used the new membership column.
The cause was not the schema change itself, but the schema version. We had written an idempotent migration that created the new column but left the core version unchanged. The production database had already applied that version, so initialization skipped the changed migration. The application tried to read or write a column it believed existed, and only the approval path failed.
We increased the version so that the migration, which was safe for an already existing column, would run again. The records from the time show that checks of the authentication boundary and a retry of new-group approval followed. The correction itself was short, but the lesson lasted.
- Application code and database changes cannot be assumed to take effect at the same moment merely because they are in the same commit.
- A rejection path that does not use a new field cannot prove that the approval path works.
- Writing idempotent DDL is not enough; the version gate that causes production to execute the change must move with it.
- Deployment verification must actually traverse the changed data path, not merely confirm that a screen opens.
A permission error and a schema error can both look like “unable to approve” from the outside. Users therefore need a message they can act on, while operators need records that reveal the boundary at which the request was rejected.
Permissions had to be understandable before they could be protected
During this period, we also organized the Korean, English, and Japanese interface and the shared toast system. At first, translation and notifications seemed like lighter finishing work than permission design. In practice, they were the final boundary that made permissions understandable to users.
If approval, rejection, save, and delete results appear in different places and with different wording on every screen, users cannot be sure their choices took effect. A notification hidden behind a popup, or a translation that omits the target and outcome in one language, can cause the same wrong action to be repeated. We therefore brought transient change results into the shared showToast() flow, while leaving field errors that require immediate correction and failures that require retry in the relevant screen.
Static UI text was registered in all three language dictionaries together, while user names, group names, documents, and raw chat were not translated arbitrarily through the UI dictionary. The language settings API was narrowed so that it changed only the language value and did not touch display names or permissions.
It was not enough for a permission policy to be accurate only on the server. A person had to understand whether they had opened a group in management view or were actually participating in it, and whether their approval had been saved. Multilingual text and shared feedback were not decoration; they were part of making authority explainable.
The Codex development collaborator still existed only in the design
On July 19, we explored a flow in which an AI pet could propose actual development work. It would be inaccurate to write that a complete execution system existed then.
What we decided that week was a boundary, not an implementation. A pet could propose work, but it did not directly receive execution authority. Development work would pass through Haru Space’s approval flow and an isolated execution environment, with a person checking both before execution and before the result was incorporated. The fact that code had been incorporated would not automatically extend authority to a production deployment.
The concrete executor and deployment connection remained future work. The principle from this stage was simple: an AI proposal, a code change, incorporation, and a production deployment are distinct permissions. As the impact of a result grows, a human approval point must appear again.
The rules that remained after this week
Separating boundaries before adding features did not simply slow development. It narrowed the area in which we had to look for a failure and gave later features a shared vocabulary. These are the practical rules we carried forward from the week’s decisions.
1. Name the resource and action before the role
Do not allow something based on the name admin alone. Viewing platform settings, proposing a group agenda item, voting, participating in chat, and deploying all concern different resources and actions.
2. Separate accounts, memberships, and roles
The ability to sign in does not mean a person belongs to a particular group. Opening the current group on screen does not create permission to act in it either.
3. Put a server check behind every hidden control
Button visibility is guidance for usability. A real boundary appears only when the server rechecks the session, active membership, and the ownership of the group and resource.
4. Divide AI results into draft, preview, and application
Normalize model output against an allowlist, then let a person inspect the actual rendered result before explicitly applying it. Actions with larger consequences, such as publication or execution, require a separate approval point.
5. Append decision history instead of overwriting it
Record not only who approved, but who was eligible at that moment and the order in which the decision was made. A reversal should be explainable as a new event rather than by deleting the past.
6. Verify a schema change through its version and path
Do not stop at checking the column and migration code. Confirm that the production version actually advances and that approval and save paths using the new field are exercised.
7. Explain permission outcomes in the user’s language
Approval, rejection, and error messages should carry the same meaning in all three languages. Show brief results through shared feedback, and keep issues that require correction or retry in their local context.
What we built before the buttons
By the end of the week, Haru Space had more buttons. Requests to join and create groups, approvals and rejections, proposals and votes, and AI space generation and application were all visible on screen.
But the real achievement was not the number of buttons.
We separated what the platform may inspect from what a group member may do. We separated human relationships from AI-pet placement. We separated a draft suggested by AI from the version applied by a person. We separated current state from the decision history that produced it.
The two failures clarified those boundaries further. The exception a super administrator needed belonged only to a particular proposal action, not to group membership as a whole. A new column did not exist in production merely because it existed in code; the version and the actual approval path had to move with it.
Looking back, group sovereignty did not mean that the platform did nothing. It meant being able to explain what the platform manages and what it does not decide on a group’s behalf, and where relationships inside the group begin and end.
Good permission design is not about creating one powerful administrator. It is about ensuring that each decision has power only in its proper place.
In the next record, I will look back on what failed when we tried to connect a talking AI pet to a real development collaborator, and how we began linking work proposals, isolated execution, human approval, commits, and deployment one boundary at a time.
Leaving a reaction may store a random identifier in this browser to prevent duplicates.

